gentle-pi 2.4.0 → 2.6.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/README.md +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { stripTypeScriptTypes } from "node:module";
|
|
5
|
+
import { runInNewContext } from "node:vm";
|
|
6
|
+
import { parseAgentClass } from "../lib/runtime-metrics.ts";
|
|
7
|
+
import { parseAgentDefinition, type AgentDefinition } from "../lib/agents-config.ts";
|
|
8
|
+
import { normalizeRpcEvent, TASK_EVENT } from "../lib/agents-protocol.ts";
|
|
9
|
+
import { lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
|
|
10
|
+
import { ChildComposition, childEvent, classifyBuiltinAgent, launchSelection } from "../lib/runtime-metrics-children.ts";
|
|
11
|
+
import { encodeNativeRuntimeEvent } from "../lib/runtime-metrics-native.ts";
|
|
12
|
+
|
|
13
|
+
await lookupPiCatalogName({ provider: "openai", modelId: "gpt-4o" });
|
|
14
|
+
const asset = new URL("../assets/agents/gentle-ai-worker.md", import.meta.url);
|
|
15
|
+
const definition = parseAgentDefinition(readFileSync(asset, "utf8"), asset.pathname, "global");
|
|
16
|
+
assert.ok("instructions" in definition);
|
|
17
|
+
const workerDefinition = definition as AgentDefinition;
|
|
18
|
+
const response = (model = "gpt-4o", native = "low") => {
|
|
19
|
+
const events = normalizeRpcEvent({ type: "message_end", message: { role: "assistant", provider: "openai",
|
|
20
|
+
model, responseModel: model, providerThinkingLevel: native, stopReason: "stop", usage: { input: 3, output: 2, reasoning: 1 } } }, { observeResponses: true });
|
|
21
|
+
const event = events.find(event => event.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
22
|
+
assert.ok(event?.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
23
|
+
return event.observation;
|
|
24
|
+
};
|
|
25
|
+
const launch = () => launchSelection(workerDefinition, { provider: "openai", id: "gpt-4o" }, "high");
|
|
26
|
+
const event = (taskId = "local-task") => childEvent("local-session", taskId, launch(), "completed", {
|
|
27
|
+
coverage: "final_assistant_messages_only", agentSettled: true, responses: [response(), response("gpt-4o-mini", "high")], droppedResponses: 2,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("installed package definitions retain classification after the actual routing transform", () => {
|
|
31
|
+
// installSddAssets/copyDirectoryFiles copies assets verbatim on first install.
|
|
32
|
+
// Isolate the real pure routing writer; do not run an installer or read user agents.
|
|
33
|
+
const source = readFileSync(new URL("../extensions/gentle-ai.ts", import.meta.url), "utf8");
|
|
34
|
+
const transform = source.match(/function updateFrontmatterRouting\([\s\S]*?\n\}/)?.[0];
|
|
35
|
+
assert.ok(transform);
|
|
36
|
+
const route = runInNewContext(`(${stripTypeScriptTypes(transform)})`);
|
|
37
|
+
for (const entry of readdirSync(new URL("../assets/agents/", import.meta.url)).filter(file => file.endsWith(".md"))) {
|
|
38
|
+
const file = entry.slice(0, -3);
|
|
39
|
+
const className = file === "sdd-proposal" ? "sdd-propose"
|
|
40
|
+
: file.startsWith("gentle-ai-") ? file.slice("gentle-ai-".length) : file;
|
|
41
|
+
const kind = parseAgentClass(className);
|
|
42
|
+
if (file === "sdd-remediate") {
|
|
43
|
+
assert.equal(kind, undefined, "remediation stays dark in the existing telemetry taxonomy");
|
|
44
|
+
const definition = parseAgentDefinition(readFileSync(new URL(`../assets/agents/${file}.md`, import.meta.url), "utf8"), file, "global");
|
|
45
|
+
assert.ok("instructions" in definition);
|
|
46
|
+
assert.equal(classifyBuiltinAgent(definition), "unknown");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
assert.ok(kind, `${file}: telemetry class`);
|
|
50
|
+
const asset = new URL(`../assets/agents/${file}.md`, import.meta.url);
|
|
51
|
+
const content = readFileSync(asset, "utf8");
|
|
52
|
+
const copied = parseAgentDefinition(content, asset.pathname, "global");
|
|
53
|
+
assert.ok("instructions" in copied);
|
|
54
|
+
assert.equal(classifyBuiltinAgent(copied), kind, "verbatim first install");
|
|
55
|
+
for (const entry of [undefined, { model: "openai/gpt-4o", thinking: "high" }]) {
|
|
56
|
+
const parsed = parseAgentDefinition(route(content, entry), asset.pathname, "global");
|
|
57
|
+
assert.ok("instructions" in parsed);
|
|
58
|
+
assert.equal(classifyBuiltinAgent(parsed), kind, `${kind}: installed routing`);
|
|
59
|
+
const customizedClass = parsed.name === "sdd-proposal" ? "sdd-propose" : parseAgentClass(parsed.name) ?? "unknown";
|
|
60
|
+
assert.equal(classifyBuiltinAgent({ ...parsed, instructions: `${parsed.instructions}\nOverride` }), customizedClass);
|
|
61
|
+
assert.equal(classifyBuiltinAgent({ ...parsed, tools: ["different-tool"] }), customizedClass);
|
|
62
|
+
assert.equal(classifyBuiltinAgent({ ...parsed, description: "different description" }), customizedClass);
|
|
63
|
+
assert.equal(classifyBuiltinAgent({ ...parsed, mode: parsed.mode === "task" ? "background" : "task" }), customizedClass);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("packaged sdd-proposal is encoded only as canonical sdd-propose", () => {
|
|
69
|
+
const path = new URL("../assets/agents/sdd-proposal.md", import.meta.url);
|
|
70
|
+
const packaged = parseAgentDefinition(readFileSync(path, "utf8"), path.pathname, "global");
|
|
71
|
+
assert.ok("instructions" in packaged);
|
|
72
|
+
const selection = launchSelection(packaged, { provider: "openai", id: "gpt-4o" }, "high");
|
|
73
|
+
assert.equal(selection.agentClass, "sdd-propose");
|
|
74
|
+
const completed = childEvent("local-session", "sdd-propose-task", selection, "completed", {
|
|
75
|
+
coverage: "final_assistant_messages_only", agentSettled: true, responses: [response()], droppedResponses: 0,
|
|
76
|
+
});
|
|
77
|
+
assert.ok(completed);
|
|
78
|
+
const composition = new ChildComposition();
|
|
79
|
+
assert.equal(composition.reserve(completed, "local-session"), true);
|
|
80
|
+
composition.record(completed);
|
|
81
|
+
const snapshot = composition.snapshot();
|
|
82
|
+
const payload = encodeNativeRuntimeEvent(snapshot.responses, snapshot.launches);
|
|
83
|
+
assert.ok(payload);
|
|
84
|
+
assert.equal(JSON.parse(payload).rows[0].agent_class, "sdd-propose");
|
|
85
|
+
assert.ok(!payload.includes("sdd-proposal"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("schema-approved packaged names survive customization without exposing private names", () => {
|
|
89
|
+
const path = new URL("../assets/agents/sdd-apply.md", import.meta.url);
|
|
90
|
+
const packaged = parseAgentDefinition(readFileSync(path, "utf8"), path.pathname, "global");
|
|
91
|
+
assert.ok("instructions" in packaged);
|
|
92
|
+
assert.equal(classifyBuiltinAgent({ ...packaged, instructions: "customized instructions" }), "sdd-apply");
|
|
93
|
+
|
|
94
|
+
assert.equal(classifyBuiltinAgent(workerDefinition), "worker");
|
|
95
|
+
// This is a packaged frontmatter name, but it is absent from the telemetry
|
|
96
|
+
// enum. Its exact fingerprint retains the compatibility mapping only.
|
|
97
|
+
assert.equal(classifyBuiltinAgent({ ...workerDefinition, instructions: "private override" }), "unknown");
|
|
98
|
+
assert.equal(classifyBuiltinAgent({ ...workerDefinition, tools: ["private tool"] }), "unknown");
|
|
99
|
+
assert.equal(classifyBuiltinAgent({ ...workerDefinition, name: "private-agent" }), "unknown");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("launch distribution and each observed combination remain independent and privacy-filtered", () => {
|
|
103
|
+
const composition = new ChildComposition();
|
|
104
|
+
const value = event();
|
|
105
|
+
assert.ok(value);
|
|
106
|
+
assert.equal(composition.reserve(value, "local-session"), true);
|
|
107
|
+
composition.record(value);
|
|
108
|
+
const view = composition.snapshot();
|
|
109
|
+
assert.equal(view.launches[0].launches, 1);
|
|
110
|
+
assert.equal(view.launches[0].selectedEffort, "high");
|
|
111
|
+
assert.equal(view.launches[0].agentClass, "worker");
|
|
112
|
+
assert.equal(view.responses.length, 2);
|
|
113
|
+
assert.deepEqual(view.responses.map(row => row.observedModelId), ["gpt-4o", "gpt-4o-mini"]);
|
|
114
|
+
assert.ok(view.responses.every(row => row.effort === "high" && row.selectedProvider === "openai"
|
|
115
|
+
&& row.selectedModelId === "gpt-4o"));
|
|
116
|
+
assert.equal(view.responses[0].providerThinkingLevel, "low");
|
|
117
|
+
assert.equal(view.responses[0].tokens.reasoning.sum, 1);
|
|
118
|
+
const encoded = encodeNativeRuntimeEvent(view.responses, view.launches);
|
|
119
|
+
assert.ok(encoded);
|
|
120
|
+
assert.ok(JSON.parse(encoded).rows.filter((row: { responses: number | null }) => row.responses !== null)
|
|
121
|
+
.every((row: { model_evidence: string; selected_effort: string }) => row.model_evidence === "response" && row.selected_effort === "high"));
|
|
122
|
+
assert.equal(view.droppedResponses, 2);
|
|
123
|
+
assert.equal(view.settled, 1);
|
|
124
|
+
assert.equal(view.statuses.completed, 1);
|
|
125
|
+
assert.ok(!JSON.stringify(view).includes("local-"));
|
|
126
|
+
const privateLaunch = launchSelection({ ...workerDefinition, instructions: "private" }, { provider: "private", id: "private-model" }, "private-effort");
|
|
127
|
+
const filtered = childEvent("local-session", "other", privateLaunch, "failed", {
|
|
128
|
+
coverage: "final_assistant_messages_only", agentSettled: false, responses: [response("private-model", "private-native")], droppedResponses: 0,
|
|
129
|
+
});
|
|
130
|
+
assert.ok(!JSON.stringify(filtered).includes("private"));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("registered child launch identity survives catalog state and missing response evidence inherits selection", () => {
|
|
134
|
+
const path = new URL("../assets/agents/sdd-explore.md", import.meta.url);
|
|
135
|
+
const agent = parseAgentDefinition(readFileSync(path, "utf8"), path.pathname, "global");
|
|
136
|
+
assert.ok("instructions" in agent);
|
|
137
|
+
const selection = launchSelection(agent, { provider: "openai-codex", id: "gpt-5.6-terra" }, "high");
|
|
138
|
+
const [observation] = normalizeRpcEvent({ type: "message_end", message: { role: "assistant", stopReason: "stop",
|
|
139
|
+
usage: { input: 3, output: 2 } } }, { observeResponses: true })
|
|
140
|
+
.filter(event => event.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
141
|
+
assert.ok(observation?.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
142
|
+
const completed = childEvent("local-session", "sdd-explore-task", selection, "completed", {
|
|
143
|
+
coverage: "final_assistant_messages_only", agentSettled: true,
|
|
144
|
+
responses: Array(5).fill(observation.observation), droppedResponses: 0,
|
|
145
|
+
});
|
|
146
|
+
assert.ok(completed);
|
|
147
|
+
const composition = new ChildComposition();
|
|
148
|
+
assert.equal(composition.reserve(completed, "local-session"), true);
|
|
149
|
+
composition.record(completed);
|
|
150
|
+
const snapshot = composition.snapshot();
|
|
151
|
+
const payload = encodeNativeRuntimeEvent(snapshot.responses, snapshot.launches);
|
|
152
|
+
assert.ok(payload);
|
|
153
|
+
const rows = JSON.parse(payload).rows;
|
|
154
|
+
const launchRow = rows.find((row: { launches: number | null }) => row.launches === 1);
|
|
155
|
+
const responseRow = rows.find((row: { responses: number | null }) => row.responses === 5);
|
|
156
|
+
assert.deepEqual(launchRow.model, { provider: "openai-codex", id: "gpt-5.6-terra" });
|
|
157
|
+
assert.equal(launchRow.model_evidence, "selected");
|
|
158
|
+
assert.equal(launchRow.selected_effort, "high");
|
|
159
|
+
assert.deepEqual(responseRow.model, { provider: "openai-codex", id: "gpt-5.6-terra" });
|
|
160
|
+
assert.equal(responseRow.model_evidence, "selected");
|
|
161
|
+
assert.equal(responseRow.selected_effort, "high");
|
|
162
|
+
assert.equal(responseRow.responses, 5);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("dedupe reserves before async policy, rejects old sessions, and survives aggregate clearing", () => {
|
|
166
|
+
const c = new ChildComposition();
|
|
167
|
+
const e = event()!;
|
|
168
|
+
assert.equal(c.reserve(e, "other-session"), false);
|
|
169
|
+
assert.equal(c.reserve(e, "local-session"), true);
|
|
170
|
+
assert.equal(c.reserve(structuredClone(e), "local-session"), false);
|
|
171
|
+
c.record(e);
|
|
172
|
+
c.clear();
|
|
173
|
+
assert.equal(c.reserve(e, "local-session"), false);
|
|
174
|
+
assert.deepEqual(c.snapshot().responses, []);
|
|
175
|
+
for (let i = 0; i < 255; i++) assert.equal(c.reserve(event(String(i))!, "local-session"), true);
|
|
176
|
+
assert.equal(c.reserve(event("overflow")!, "local-session"), false);
|
|
177
|
+
assert.equal(c.snapshot().saturated, true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("response capacity retains launch ranking and reports excluded responses without eviction", () => {
|
|
181
|
+
const c = new ChildComposition();
|
|
182
|
+
for (let i = 0; i < 9; i++) {
|
|
183
|
+
const e = childEvent("local-session", String(i), launch(), "completed", {
|
|
184
|
+
coverage: "final_assistant_messages_only", agentSettled: true, responses: Array(128).fill(response()), droppedResponses: 0,
|
|
185
|
+
})!;
|
|
186
|
+
assert.equal(c.reserve(e, "local-session"), true);
|
|
187
|
+
c.record(e);
|
|
188
|
+
c.record(e);
|
|
189
|
+
}
|
|
190
|
+
const view = c.snapshot();
|
|
191
|
+
assert.equal(view.responses[0].responses, 1024);
|
|
192
|
+
assert.equal(view.launches[0].launches, 9);
|
|
193
|
+
assert.equal(view.droppedResponses, 128);
|
|
194
|
+
assert.equal(view.saturated, true);
|
|
195
|
+
assert.equal(view.statuses.completed, 9);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("malformed and oversized events are rejected without reserving IDs", () => {
|
|
199
|
+
const c = new ChildComposition();
|
|
200
|
+
const e = event()!;
|
|
201
|
+
for (const patch of [{ schema: "old" }, { responses: Array(129).fill(e.responses[0]) }, { taskId: "x".repeat(129) },
|
|
202
|
+
{ droppedResponses: -1 }, { status: "native_success" }, { launch: { ...e.launch, agentClass: "private" } }]) {
|
|
203
|
+
assert.equal(c.reserve({ ...e, ...patch }, "local-session"), false);
|
|
204
|
+
}
|
|
205
|
+
assert.equal(c.reserve(e, "local-session"), true);
|
|
206
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import * as delivery from "../lib/runtime-metrics-delivery.ts";
|
|
4
|
+
|
|
5
|
+
const tick = () => new Promise<void>(resolve => setImmediate(resolve));
|
|
6
|
+
|
|
7
|
+
test("callback returns before a blocked attempt; busy events are never queued", async () => {
|
|
8
|
+
const owner = new delivery.RuntimeMetricsAttempt();
|
|
9
|
+
let calls = 0;
|
|
10
|
+
let finish!: () => void;
|
|
11
|
+
assert.equal(owner.offer(() => { calls++; return new Promise<void>(resolve => { finish = resolve; }); }), true);
|
|
12
|
+
assert.equal(calls, 0, "provider callback returns before any transport work");
|
|
13
|
+
assert.equal(owner.offer(async () => { calls++; }), false);
|
|
14
|
+
await tick();
|
|
15
|
+
assert.equal(calls, 1);
|
|
16
|
+
assert.equal(new delivery.RuntimeMetricsAttempt().offer(async () => { calls++; }), false);
|
|
17
|
+
finish(); await tick(); await tick();
|
|
18
|
+
assert.equal(calls, 1, "busy events never replay");
|
|
19
|
+
owner.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("failure is silent and the next fresh event may attempt without cooldown", async () => {
|
|
23
|
+
const owner = new delivery.RuntimeMetricsAttempt();
|
|
24
|
+
let calls = 0;
|
|
25
|
+
owner.offer(async () => { calls++; throw new Error("private failure"); });
|
|
26
|
+
await tick(); await tick();
|
|
27
|
+
assert.equal(calls, 1);
|
|
28
|
+
assert.equal(owner.offer(async () => { calls++; }), true);
|
|
29
|
+
await tick(); await tick();
|
|
30
|
+
assert.equal(calls, 2);
|
|
31
|
+
owner.dispose();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("replacement cancels an unstarted attempt without stale execution", async () => {
|
|
35
|
+
const owner = new delivery.RuntimeMetricsAttempt();
|
|
36
|
+
let calls = 0;
|
|
37
|
+
owner.offer(async () => { calls++; });
|
|
38
|
+
owner.dispose(); owner.dispose();
|
|
39
|
+
assert.equal(owner.offer(async () => { calls++; }), false);
|
|
40
|
+
const replacement = new delivery.RuntimeMetricsAttempt();
|
|
41
|
+
assert.equal(replacement.offer(async () => { calls++; }), true);
|
|
42
|
+
await tick(); await tick();
|
|
43
|
+
assert.equal(calls, 1);
|
|
44
|
+
replacement.dispose();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("shutdown aborts immediately but cannot overlap an unsettled process", async () => {
|
|
48
|
+
const owner = new delivery.RuntimeMetricsAttempt();
|
|
49
|
+
let signal!: AbortSignal;
|
|
50
|
+
let finish!: () => void;
|
|
51
|
+
owner.offer(s => { signal = s; return new Promise<void>(resolve => { finish = resolve; }); });
|
|
52
|
+
await tick();
|
|
53
|
+
assert.equal(owner.dispose(), undefined);
|
|
54
|
+
assert.equal(signal.aborted, true);
|
|
55
|
+
const replacement = new delivery.RuntimeMetricsAttempt();
|
|
56
|
+
assert.equal(replacement.offer(async () => {}), false);
|
|
57
|
+
finish(); await tick();
|
|
58
|
+
assert.equal(replacement.offer(async () => {}), true);
|
|
59
|
+
await tick(); replacement.dispose();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("bounded shutdown join waits for the accepted attempt without replaying busy work", async () => {
|
|
63
|
+
const owner = new delivery.RuntimeMetricsAttempt();
|
|
64
|
+
let calls = 0;
|
|
65
|
+
let finish!: () => void;
|
|
66
|
+
owner.offer(() => { calls++; return new Promise<void>(resolve => { finish = resolve; }); });
|
|
67
|
+
assert.equal(owner.offer(async () => { calls++; }), false);
|
|
68
|
+
await tick();
|
|
69
|
+
let joined = false;
|
|
70
|
+
const joining = owner.waitForSettled(50).then(() => { joined = true; });
|
|
71
|
+
await tick();
|
|
72
|
+
assert.equal(joined, false);
|
|
73
|
+
finish();
|
|
74
|
+
await joining;
|
|
75
|
+
assert.equal(calls, 1, "joining never queues or retries discarded work");
|
|
76
|
+
owner.dispose();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("bounded shutdown join returns after its deadline", async () => {
|
|
80
|
+
const owner = new delivery.RuntimeMetricsAttempt();
|
|
81
|
+
owner.offer(() => new Promise<void>(() => {}));
|
|
82
|
+
await tick();
|
|
83
|
+
await owner.waitForSettled(5);
|
|
84
|
+
owner.dispose();
|
|
85
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import runtimeMetrics from "../extensions/runtime-metrics.ts";
|
|
4
|
+
import { parseAgentClass, type RuntimeMetricBucket } from "../lib/runtime-metrics.ts";
|
|
5
|
+
import { createPiCatalogNameLookup, type PiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
|
|
6
|
+
import { CHILD_METRICS_EVENT, childEvent } from "../lib/runtime-metrics-children.ts";
|
|
7
|
+
import { normalizeRpcEvent, TASK_EVENT } from "../lib/agents-protocol.ts";
|
|
8
|
+
|
|
9
|
+
const tick = () => new Promise<void>(resolve => setImmediate(resolve));
|
|
10
|
+
type CatalogLookup = NonNullable<NonNullable<Parameters<typeof runtimeMetrics>[2]>["lookup"]>;
|
|
11
|
+
function harness(env: NodeJS.ProcessEnv = {}, lookup?: CatalogLookup,
|
|
12
|
+
classify?: (input: unknown) => PiCatalogName, mode: "tui" | "print" = "tui", shutdownWaitMs = 1500) {
|
|
13
|
+
const handlers = new Map<string, Function>();
|
|
14
|
+
const listeners = new Map<string, Function>();
|
|
15
|
+
let session = "first";
|
|
16
|
+
let finish!: () => void;
|
|
17
|
+
let signal!: AbortSignal;
|
|
18
|
+
const sent: RuntimeMetricBucket[][] = [];
|
|
19
|
+
const launches: unknown[] = [];
|
|
20
|
+
const ctx: any = { cwd: "/fixture", mode, model: { provider: "openai", id: "gpt-4o" },
|
|
21
|
+
sessionManager: { getSessionId: () => session, getEntries: () => assert.fail("no reconstruction") } };
|
|
22
|
+
const pi: any = { on: (name: string, handler: Function) => handlers.set(name, handler),
|
|
23
|
+
getThinkingLevel: () => "high", registerCommand() {},
|
|
24
|
+
appendEntry: () => assert.fail("no metrics persistence"),
|
|
25
|
+
events: { on: (name: string, handler: Function) => { listeners.set(name, handler); return () => listeners.delete(name); } } };
|
|
26
|
+
runtimeMetrics(pi, env, { lookup, classify, now: () => 0, shutdownWaitMs, send: (rows, _cwd, deps) => {
|
|
27
|
+
sent.push(structuredClone(rows)); launches.push(structuredClone(deps?.launches)); signal = deps!.signal!;
|
|
28
|
+
return new Promise(resolve => { finish = () => resolve("discarded"); });
|
|
29
|
+
} });
|
|
30
|
+
return { sent, launches, ctx, handlers,
|
|
31
|
+
emit: (name: string, event: any = {}) => handlers.get(name)?.(event, ctx),
|
|
32
|
+
bus: (event: unknown) => listeners.get(CHILD_METRICS_EVENT)?.(event),
|
|
33
|
+
replace: () => { session = "second"; }, signal: () => signal,
|
|
34
|
+
finish: async () => { finish(); await tick(); },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const final = (input = 7) => ({ role: "assistant", provider: "openai", model: "gpt-4o", responseModel: "gpt-4o",
|
|
38
|
+
providerThinkingLevel: "low", stopReason: "stop", usage: { input, output: 3, cacheRead: 0 },
|
|
39
|
+
content: [{ text: "private response" }], errorMessage: "private error", path: "/private" });
|
|
40
|
+
|
|
41
|
+
test("message classification retries a failed catalog load without awaiting it", async () => {
|
|
42
|
+
let calls = 0;
|
|
43
|
+
const publicName = Object.freeze({ classification: "catalog_public", modelId: "gpt-4o" }) satisfies PiCatalogName;
|
|
44
|
+
const catalog = new Map([[JSON.stringify(["openai", "gpt-4o"]), publicName]]);
|
|
45
|
+
const catalogLookup = createPiCatalogNameLookup(async () => {
|
|
46
|
+
calls++;
|
|
47
|
+
if (calls === 1) throw new Error("transient catalog failure");
|
|
48
|
+
return catalog;
|
|
49
|
+
});
|
|
50
|
+
const h = harness({}, catalogLookup.lookup, catalogLookup.classify);
|
|
51
|
+
h.emit("session_start");
|
|
52
|
+
await tick();
|
|
53
|
+
assert.equal(calls, 1);
|
|
54
|
+
assert.equal(h.emit("message_end", { message: final() }), undefined);
|
|
55
|
+
assert.equal(calls, 2);
|
|
56
|
+
await tick();
|
|
57
|
+
assert.equal(h.sent.length, 1);
|
|
58
|
+
assert.equal(h.sent[0][0].observedModelId, "unknown", "the racing row remains fail-closed");
|
|
59
|
+
await h.finish();
|
|
60
|
+
h.emit("message_end", { message: final(8) });
|
|
61
|
+
await tick();
|
|
62
|
+
assert.equal(h.sent[1][0].observedModelId, "gpt-4o", "a later row uses the recovered catalog");
|
|
63
|
+
await h.finish();
|
|
64
|
+
h.emit("session_shutdown");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("final callback returns before blocked transport; busy and duplicate messages drop", async () => {
|
|
68
|
+
const h = harness(); await h.emit("session_start");
|
|
69
|
+
h.emit("turn_start"); h.emit("before_provider_request");
|
|
70
|
+
const message = final();
|
|
71
|
+
assert.equal(h.emit("message_end", { message }), undefined);
|
|
72
|
+
assert.equal(h.sent.length, 0);
|
|
73
|
+
h.emit("message_end", { message });
|
|
74
|
+
h.emit("message_end", { message: final(99) });
|
|
75
|
+
await tick(); assert.equal(h.sent.length, 1);
|
|
76
|
+
assert.equal(h.sent[0][0].tokens.input.sum, 7);
|
|
77
|
+
assert.equal(h.sent[0][0].effort, "high");
|
|
78
|
+
assert.equal(h.sent[0][0].providerThinkingLevel, "low");
|
|
79
|
+
assert.ok(!JSON.stringify(h.sent).includes("private"));
|
|
80
|
+
await h.finish(); await tick();
|
|
81
|
+
assert.equal(h.sent.length, 1, "no replay after failure");
|
|
82
|
+
h.emit("message_end", { message: final(2) }); await tick();
|
|
83
|
+
assert.equal(h.sent[1][0].tokens.input.sum, 2, "event usage, never cumulative totals");
|
|
84
|
+
assert.equal(h.sent[1][0].responses, 1);
|
|
85
|
+
await h.finish(); h.emit("session_shutdown");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("replacement before launch cancels stale work; shutdown does not await a blocked child", async () => {
|
|
89
|
+
const h = harness(); await h.emit("session_start");
|
|
90
|
+
h.emit("message_end", { message: final() });
|
|
91
|
+
h.replace(); await h.emit("session_start"); await tick();
|
|
92
|
+
assert.equal(h.sent.length, 0);
|
|
93
|
+
h.emit("message_end", { message: final() }); await tick();
|
|
94
|
+
assert.equal(h.emit("session_shutdown"), undefined);
|
|
95
|
+
assert.equal(h.signal().aborted, true);
|
|
96
|
+
await h.finish();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("print shutdown joins an accepted orchestrator delivery before disposing", async () => {
|
|
100
|
+
const h = harness({}, undefined, undefined, "print", 50); await h.emit("session_start");
|
|
101
|
+
h.emit("message_end", { message: final() }); await tick();
|
|
102
|
+
let stopped = false;
|
|
103
|
+
const shutdown = h.emit("session_shutdown").then(() => { stopped = true; });
|
|
104
|
+
await tick();
|
|
105
|
+
assert.equal(stopped, false);
|
|
106
|
+
assert.equal(h.signal().aborted, false);
|
|
107
|
+
await h.finish(); await shutdown;
|
|
108
|
+
assert.equal(stopped, true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("print shutdown joins an accepted child launch and response delivery", async () => {
|
|
112
|
+
const h = harness({}, undefined, undefined, "print", 50); await h.emit("session_start");
|
|
113
|
+
const observation = normalizeRpcEvent({ type: "message_end", message: final() }, { observeResponses: true })
|
|
114
|
+
.find(event => event.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
115
|
+
assert.ok(observation?.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
116
|
+
h.bus(childEvent("first", "child", {
|
|
117
|
+
agentClass: parseAgentClass("sdd-explore")!, selectedProvider: "openai", selectedModelId: "gpt-4o", selectedEffort: "high",
|
|
118
|
+
}, "completed", { coverage: "final_assistant_messages_only", agentSettled: true, droppedResponses: 0,
|
|
119
|
+
responses: [observation.observation] }, 0)!);
|
|
120
|
+
await tick();
|
|
121
|
+
const shutdown = h.emit("session_shutdown");
|
|
122
|
+
assert.equal(h.sent[0][0].agentClass, "sdd-explore");
|
|
123
|
+
assert.deepEqual(h.launches[0], [{ evidence: "launch_configuration", agentClass: "sdd-explore", selectedProvider: "openai",
|
|
124
|
+
selectedModelId: "gpt-4o", selectedEffort: "high", launches: 1 }]);
|
|
125
|
+
assert.equal(h.signal().aborted, false);
|
|
126
|
+
await h.finish(); await shutdown;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("print shutdown aborts a delivery after the bounded join deadline", async () => {
|
|
130
|
+
const h = harness({}, undefined, undefined, "print", 5); await h.emit("session_start");
|
|
131
|
+
h.emit("message_end", { message: final() }); await tick();
|
|
132
|
+
await h.emit("session_shutdown");
|
|
133
|
+
assert.equal(h.signal().aborted, true);
|
|
134
|
+
await h.finish();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("interactive shutdown never joins a blocked delivery", async () => {
|
|
138
|
+
const h = harness({}, undefined, undefined, "tui", 50); await h.emit("session_start");
|
|
139
|
+
h.emit("message_end", { message: final() }); await tick();
|
|
140
|
+
assert.equal(h.emit("session_shutdown"), undefined);
|
|
141
|
+
assert.equal(h.signal().aborted, true);
|
|
142
|
+
await h.finish();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const env of [{ DO_NOT_TRACK: "1" }, { GENTLE_AI_TELEMETRY: "0" }, { CI: "true" }, { GENTLE_PI_AGENTS_CHILD: "1" }]) {
|
|
146
|
+
test(`opt-out suppresses hooks and launches: ${Object.keys(env)[0]}`, async () => {
|
|
147
|
+
const h = harness(env); await h.emit("session_start");
|
|
148
|
+
h.emit("message_end", { message: final() }); await tick();
|
|
149
|
+
assert.equal(h.handlers.size, 0); assert.equal(h.sent.length, 0);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
test("environment veto between callback and launch discards accepted event", async () => {
|
|
154
|
+
const env: NodeJS.ProcessEnv = {};
|
|
155
|
+
const h = harness(env); await h.emit("session_start");
|
|
156
|
+
h.emit("message_end", { message: final() }); env.DO_NOT_TRACK = "1";
|
|
157
|
+
await tick(); assert.equal(h.sent.length, 0); h.emit("session_shutdown");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("tool and partial assistant messages are not consumed as primary usage", async () => {
|
|
161
|
+
const h = harness(); await h.emit("session_start");
|
|
162
|
+
for (const message of [{ ...final(), role: "toolResult" }, { ...final(), stopReason: "pending" }, { ...final(), stopReason: "deferred" }]) h.emit("message_end", { message });
|
|
163
|
+
await tick(); assert.equal(h.sent.length, 0); h.emit("session_shutdown");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("child completion consumed once, busy children drop, and primary usage stays separate", async () => {
|
|
167
|
+
const h = harness(); await h.emit("session_start");
|
|
168
|
+
const observation = normalizeRpcEvent({ type: "message_end", message: final() }, { observeResponses: true })
|
|
169
|
+
.find(event => event.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
170
|
+
assert.ok(observation?.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
171
|
+
const event = (taskId: string) => childEvent("first", taskId,
|
|
172
|
+
{ agentClass: parseAgentClass("verify")!, selectedProvider: "openai", selectedModelId: "gpt-4o", selectedEffort: "high" }, "completed",
|
|
173
|
+
{ coverage: "final_assistant_messages_only", agentSettled: true, droppedResponses: 0, responses: [observation.observation] }, 0)!;
|
|
174
|
+
h.bus(event("one")); h.bus(event("one")); h.bus(event("busy"));
|
|
175
|
+
await tick(); assert.equal(h.sent.length, 1);
|
|
176
|
+
assert.equal(h.sent[0][0].agentClass, "verify");
|
|
177
|
+
assert.deepEqual(h.launches[0], [{ evidence: "launch_configuration", agentClass: "verify", selectedProvider: "openai",
|
|
178
|
+
selectedModelId: "gpt-4o", selectedEffort: "high", launches: 1 }]);
|
|
179
|
+
assert.equal(h.sent[0][0].effort, "high", "child response retains launch selection separately from response evidence");
|
|
180
|
+
assert.equal(h.sent[0][0].providerThinkingLevel, "low");
|
|
181
|
+
await h.finish(); h.bus(event("busy")); await tick();
|
|
182
|
+
assert.equal(h.sent.length, 1, "busy completion stays consumed");
|
|
183
|
+
h.emit("message_end", { message: final(2) }); await tick();
|
|
184
|
+
assert.equal(h.sent[1][0].agentClass, "orchestrator");
|
|
185
|
+
assert.equal(h.sent[1][0].tokens.input.sum, 2);
|
|
186
|
+
await h.finish(); h.emit("session_shutdown");
|
|
187
|
+
});
|