@cassiomc1/forgeloop 1.6.4 → 1.7.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/AGENT_COMPATIBILITY.md +11 -0
- package/DOCS_INDEX.md +1 -0
- package/GUIDE_ROUTER.md +26 -1
- package/LOOP_ENGINEERING.md +48 -0
- package/ORCHESTRATOR_INTEGRATION.md +38 -0
- package/PROTOCOL_INTEGRATION.md +62 -0
- package/README.md +25 -0
- package/benchmarks/execution-profiles/README.md +44 -0
- package/benchmarks/execution-profiles/api-feature.json +18 -0
- package/benchmarks/execution-profiles/authentication-change.json +18 -0
- package/benchmarks/execution-profiles/documentation-correction.json +18 -0
- package/benchmarks/execution-profiles/infrastructure-release.json +18 -0
- package/benchmarks/execution-profiles/novatask-saas-landing-page.json +36 -0
- package/benchmarks/execution-profiles/small-bug-fix.json +18 -0
- package/benchmarks/execution-profiles/static-landing-page.json +18 -0
- package/completions/_forgeloop +6 -4
- package/completions/forgeloop.bash +8 -4
- package/completions/forgeloop.fish +24 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +45 -1
- package/docs/ARTIFACT_REFERENCE.md +53 -7
- package/docs/CLI_REFERENCE.md +83 -1
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +208 -0
- package/docs/GETTING_STARTED.md +28 -0
- package/docs/MCP.md +6 -0
- package/docs/RELEASE_CHECKLIST.md +4 -0
- package/docs/TROUBLESHOOTING.md +6 -0
- package/docs/UNIVERSAL_INTEGRATION.md +58 -0
- package/package.json +14 -2
- package/schemas/config.schema.json +1 -0
- package/schemas/execution-profile-benchmark-aggregate.schema.json +43 -0
- package/schemas/execution-profile-benchmark-run.schema.json +106 -0
- package/schemas/execution-profile-benchmark-scenario.schema.json +66 -0
- package/schemas/routing-result.schema.json +12 -0
- package/schemas/usage.schema.json +30 -0
- package/scripts/check-efficiency-regression.mjs +99 -0
- package/scripts/generate-agent-protocol-summary.mjs +35 -0
- package/scripts/lib/execution-profile-benchmark-io.mjs +67 -0
- package/scripts/run-execution-profile-benchmarks.mjs +265 -0
- package/scripts/summarize-execution-profile-benchmarks.mjs +84 -0
- package/scripts/validate-execution-profile-benchmarks.mjs +120 -0
- package/src/cli.js +16 -4
- package/src/commands/efficiency.js +12 -0
- package/src/commands/eval.js +8 -2
- package/src/commands/metrics.js +2 -2
- package/src/commands/next.js +26 -2
- package/src/commands/route.js +20 -2
- package/src/commands/task-show.js +31 -2
- package/src/commands/usage-record.js +61 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/cli-command-definitions.js +27 -0
- package/src/core/command-executors.js +29 -5
- package/src/core/command-input.js +34 -0
- package/src/core/config.js +9 -0
- package/src/core/efficiency.js +197 -0
- package/src/core/error-codes.js +18 -0
- package/src/core/execution-profile-benchmarks.js +674 -0
- package/src/core/execution-profile-context.js +177 -0
- package/src/core/execution-profile.js +248 -0
- package/src/core/integration-invocation-policy.js +43 -0
- package/src/core/integration-resources.js +22 -1
- package/src/core/protocol-info.js +42 -0
- package/src/core/resumability.js +27 -1
- package/src/core/router.js +23 -1
- package/src/core/runtime-context.js +11 -0
- package/src/core/schema-validation.js +4 -0
- package/src/core/task-paths.js +2 -0
- package/src/core/templates.js +4 -0
- package/src/core/trace.js +1 -0
- package/src/core/trajectory-evaluation.js +2 -2
- package/src/core/trajectory-metrics.js +18 -2
- package/src/core/usage.js +137 -0
- package/src/integration.d.ts +84 -0
- package/src/integration.js +14 -0
package/src/core/router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GUIDE_IDS, PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
import { assertExecutionProfile, resolveExecutionProfile } from "./execution-profile.js";
|
|
2
3
|
|
|
3
4
|
export const ROUTING_SCHEMA_VERSION = 1;
|
|
4
5
|
|
|
@@ -50,6 +51,12 @@ const SIGNALS = Object.freeze({
|
|
|
50
51
|
"critical-path",
|
|
51
52
|
"performance",
|
|
52
53
|
"accessibility",
|
|
54
|
+
"destructive",
|
|
55
|
+
"irreversible",
|
|
56
|
+
"production-deployment",
|
|
57
|
+
"credentials",
|
|
58
|
+
"payment",
|
|
59
|
+
"migration",
|
|
53
60
|
]),
|
|
54
61
|
platforms: new Set(["web", "mobile", "desktop", "server", "ci", "cross-platform"]),
|
|
55
62
|
});
|
|
@@ -167,7 +174,7 @@ export function normalizeRouteInput(input = {}) {
|
|
|
167
174
|
};
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
export function evaluateRoute(input = {}) {
|
|
177
|
+
export function evaluateRoute(input = {}, profileOptions = {}) {
|
|
171
178
|
const normalized = normalizeRouteInput(input);
|
|
172
179
|
const selected = new Map();
|
|
173
180
|
const excluded = {};
|
|
@@ -259,6 +266,14 @@ export function evaluateRoute(input = {}) {
|
|
|
259
266
|
guides,
|
|
260
267
|
reasons: Object.fromEntries(guides.map((guide) => [guide, selected.get(guide)])),
|
|
261
268
|
excluded,
|
|
269
|
+
executionProfile: resolveExecutionProfile({
|
|
270
|
+
routeInput: normalized,
|
|
271
|
+
contract: profileOptions.contract ?? input.contract ?? null,
|
|
272
|
+
taskDescriptor: profileOptions.taskDescriptor ?? input.taskDescriptor ?? null,
|
|
273
|
+
configuredProfile: profileOptions.configuredProfile ?? input.configuredProfile ?? "auto",
|
|
274
|
+
requestedProfile: profileOptions.requestedProfile
|
|
275
|
+
?? (Object.prototype.hasOwnProperty.call(input, "executionProfile") ? input.executionProfile : null),
|
|
276
|
+
}),
|
|
262
277
|
};
|
|
263
278
|
return assertRouteInvariants(result);
|
|
264
279
|
}
|
|
@@ -278,6 +293,13 @@ export function assertRouteInvariants(result) {
|
|
|
278
293
|
if (result.contractFingerprint !== undefined && !/^[a-f0-9]{64}$/.test(result.contractFingerprint)) {
|
|
279
294
|
throw invariantError("E_ROUTE_INVALID", "Route contractFingerprint must be a lowercase SHA-256 fingerprint");
|
|
280
295
|
}
|
|
296
|
+
if (result.executionProfile !== undefined) {
|
|
297
|
+
try {
|
|
298
|
+
assertExecutionProfile(result.executionProfile);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw invariantError(error.code ?? "E_EXECUTION_PROFILE_INCONSISTENT", error.message);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
281
303
|
for (const guide of result.guides) {
|
|
282
304
|
if (!GUIDE_IDS.includes(guide)) throw invariantError("E_ROUTE_INVALID", `Route result contains unknown guide: ${guide}`);
|
|
283
305
|
if (!Array.isArray(result.reasons?.[guide]) || result.reasons[guide].length === 0) {
|
|
@@ -94,5 +94,16 @@ export function createForgeLoopContext(options = {}) {
|
|
|
94
94
|
if (options?.verificationExecutionPolicy !== undefined) {
|
|
95
95
|
context.verificationExecutionPolicy = normalizeVerificationExecutionPolicy(options.verificationExecutionPolicy);
|
|
96
96
|
}
|
|
97
|
+
if (options?.usageProvider !== undefined) {
|
|
98
|
+
if (!options.usageProvider
|
|
99
|
+
|| typeof options.usageProvider !== "object"
|
|
100
|
+
|| Array.isArray(options.usageProvider)
|
|
101
|
+
|| typeof options.usageProvider.getTaskUsage !== "function") {
|
|
102
|
+
const error = new Error("Usage provider must expose getTaskUsage({ projectPath, taskId })");
|
|
103
|
+
error.code = "E_USAGE_INVALID";
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
context.usageProvider = options.usageProvider;
|
|
107
|
+
}
|
|
97
108
|
return Object.freeze(context);
|
|
98
109
|
}
|
|
@@ -50,6 +50,10 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
|
|
|
50
50
|
"code-attestation",
|
|
51
51
|
"attestation-verification-result",
|
|
52
52
|
"in-toto-statement",
|
|
53
|
+
"usage",
|
|
54
|
+
"execution-profile-benchmark-scenario",
|
|
55
|
+
"execution-profile-benchmark-run",
|
|
56
|
+
"execution-profile-benchmark-aggregate",
|
|
53
57
|
]);
|
|
54
58
|
|
|
55
59
|
export class SchemaValidationError extends Error {
|
package/src/core/task-paths.js
CHANGED
|
@@ -18,6 +18,7 @@ export const TASK_ARTIFACT_FILES = Object.freeze({
|
|
|
18
18
|
actions: "actions",
|
|
19
19
|
approvals: "approvals",
|
|
20
20
|
evaluations: "evaluations",
|
|
21
|
+
usage: "usage.json",
|
|
21
22
|
policySnapshot: "policy-snapshot.json",
|
|
22
23
|
recovery: "recovery.json",
|
|
23
24
|
workspaceBinding: "workspace-binding.json",
|
|
@@ -193,6 +194,7 @@ export function buildTaskArtifactPaths(taskId) {
|
|
|
193
194
|
actions: `${dir}/${TASK_ARTIFACT_FILES.actions}`,
|
|
194
195
|
approvals: `${dir}/${TASK_ARTIFACT_FILES.approvals}`,
|
|
195
196
|
evaluations: `${dir}/${TASK_ARTIFACT_FILES.evaluations}`,
|
|
197
|
+
usage: `${dir}/${TASK_ARTIFACT_FILES.usage}`,
|
|
196
198
|
lock: taskLockPath(taskId),
|
|
197
199
|
policySnapshot: `${dir}/${TASK_ARTIFACT_FILES.policySnapshot}`,
|
|
198
200
|
recovery: `${dir}/${TASK_ARTIFACT_FILES.recovery}`,
|
package/src/core/templates.js
CHANGED
|
@@ -83,6 +83,10 @@ export const TEMPLATE_PATHS = [
|
|
|
83
83
|
"schemas/code-attestation.schema.json",
|
|
84
84
|
"schemas/attestation-verification-result.schema.json",
|
|
85
85
|
"schemas/in-toto-statement.schema.json",
|
|
86
|
+
"schemas/usage.schema.json",
|
|
87
|
+
"schemas/execution-profile-benchmark-scenario.schema.json",
|
|
88
|
+
"schemas/execution-profile-benchmark-run.schema.json",
|
|
89
|
+
"schemas/execution-profile-benchmark-aggregate.schema.json",
|
|
86
90
|
];
|
|
87
91
|
|
|
88
92
|
export function getPackageRoot() {
|
package/src/core/trace.js
CHANGED
|
@@ -12,7 +12,7 @@ import { withTaskTransaction } from "./transaction.js";
|
|
|
12
12
|
|
|
13
13
|
function evaluationError(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
14
14
|
|
|
15
|
-
export async function evaluateTrajectory({ target, packageRoot, taskId, scenarioPath, evaluationId = `eval-${randomUUID()}
|
|
15
|
+
export async function evaluateTrajectory({ target, packageRoot, taskId, scenarioPath, evaluationId = `eval-${randomUUID()}`, runtimeContext = null }) {
|
|
16
16
|
if (typeof scenarioPath !== "string" || !scenarioPath || path.isAbsolute(scenarioPath) || scenarioPath.includes("..")) {
|
|
17
17
|
throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", "scenario path must be a relative project-local file");
|
|
18
18
|
}
|
|
@@ -26,7 +26,7 @@ export async function evaluateTrajectory({ target, packageRoot, taskId, scenario
|
|
|
26
26
|
assertSchema(scenario, await readSchema("trajectory-scenario", packageRoot), "trajectory scenario");
|
|
27
27
|
if (!/^eval-[A-Za-z0-9_-]+$/.test(evaluationId)) throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", "evaluationId is invalid");
|
|
28
28
|
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
29
|
-
const metrics = await buildTrajectoryMetrics({ target, packageRoot, taskId });
|
|
29
|
+
const metrics = await buildTrajectoryMetrics({ target, packageRoot, taskId, runtimeContext });
|
|
30
30
|
const milestoneSet = new Set(trace.events.map((event) => event.type));
|
|
31
31
|
const missingMilestones = (scenario.requiredMilestones ?? []).filter((milestone) => !milestoneSet.has(milestone));
|
|
32
32
|
const completionEvent = trace.events.find((event) => event.type === "COMPLETION_VALIDATED");
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { buildTaskTrace } from "./trace.js";
|
|
2
2
|
import { buildTaskReflection } from "./reflection.js";
|
|
3
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
4
|
+
import { providerUsage, readTaskUsage, unknownUsage } from "./usage.js";
|
|
5
|
+
import { projectExecutionProfile } from "./execution-profile.js";
|
|
3
6
|
|
|
4
7
|
export const COMPARABLE_WORK_EVENTS = new Set([
|
|
5
8
|
"EXECUTION_STARTED", "VERIFICATION_RECORDED", "DIAGNOSTIC_CASE_RECORDED",
|
|
6
9
|
"INTERVENTION_RECORDED", "ACTION_STARTED", "ACTION_RECONCILED", "REVIEW_STARTED",
|
|
7
10
|
]);
|
|
8
11
|
|
|
9
|
-
export async function buildTrajectoryMetrics({ target, packageRoot, taskId }) {
|
|
12
|
+
export async function buildTrajectoryMetrics({ target, packageRoot, taskId, runtimeContext = null }) {
|
|
10
13
|
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
11
14
|
const reflection = await buildTaskReflection({ target, packageRoot, taskId });
|
|
12
15
|
const events = trace.events;
|
|
@@ -18,6 +21,18 @@ export async function buildTrajectoryMetrics({ target, packageRoot, taskId }) {
|
|
|
18
21
|
const firstMs = firstEventAt ? Date.parse(firstEventAt) : NaN;
|
|
19
22
|
const lastMs = lastEventAt ? Date.parse(lastEventAt) : NaN;
|
|
20
23
|
const interventions = reflection.interventions ?? { count: 0, informative: 0, nonInformative: 0 };
|
|
24
|
+
let executionProfile = null;
|
|
25
|
+
try {
|
|
26
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId });
|
|
27
|
+
executionProfile = projectExecutionProfile(route.value);
|
|
28
|
+
} catch {
|
|
29
|
+
// Legacy tasks and incomplete task namespaces may not have a route.
|
|
30
|
+
}
|
|
31
|
+
let usage = await readTaskUsage(target, packageRoot, taskId);
|
|
32
|
+
const usageProvider = runtimeContext?.usageProvider;
|
|
33
|
+
if (usageProvider && typeof usageProvider.getTaskUsage === "function") {
|
|
34
|
+
usage = providerUsage(await usageProvider.getTaskUsage({ projectPath: target, taskId }));
|
|
35
|
+
}
|
|
21
36
|
return {
|
|
22
37
|
schemaVersion: 1,
|
|
23
38
|
taskId,
|
|
@@ -74,7 +89,8 @@ export async function buildTrajectoryMetrics({ target, packageRoot, taskId }) {
|
|
|
74
89
|
lastEventAt,
|
|
75
90
|
wallClockMs: Number.isFinite(firstMs) && Number.isFinite(lastMs) ? Math.max(0, lastMs - firstMs) : null,
|
|
76
91
|
},
|
|
77
|
-
|
|
92
|
+
executionProfile,
|
|
93
|
+
usage: usage ?? unknownUsage(),
|
|
78
94
|
comparableSteps: events.filter((event) => COMPARABLE_WORK_EVENTS.has(event.type)).length,
|
|
79
95
|
};
|
|
80
96
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
3
|
+
|
|
4
|
+
export const USAGE_SOURCES = Object.freeze([
|
|
5
|
+
"PROVIDER_REPORTED",
|
|
6
|
+
"HOST_REPORTED",
|
|
7
|
+
"ACTOR_REPORTED",
|
|
8
|
+
"UNKNOWN",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const USAGE_FIELDS = Object.freeze([
|
|
12
|
+
"inputTokens",
|
|
13
|
+
"outputTokens",
|
|
14
|
+
"cacheReadTokens",
|
|
15
|
+
"cacheWriteTokens",
|
|
16
|
+
"totalTokens",
|
|
17
|
+
"costUsd",
|
|
18
|
+
"model",
|
|
19
|
+
"provider",
|
|
20
|
+
"source",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function usageError(code, message) {
|
|
24
|
+
const error = new Error(message);
|
|
25
|
+
error.code = code;
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function nullableNonNegativeInteger(value, label) {
|
|
30
|
+
if (value === null || value === undefined || value === "") return null;
|
|
31
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
32
|
+
throw usageError("E_USAGE_INVALID", `${label} must be a non-negative integer or null`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nullableCost(value) {
|
|
38
|
+
if (value === null || value === undefined || value === "") return null;
|
|
39
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
40
|
+
if (!Number.isFinite(numeric) || numeric < 0) {
|
|
41
|
+
throw usageError("E_USAGE_INVALID", "costUsd must be a non-negative finite number or null");
|
|
42
|
+
}
|
|
43
|
+
return numeric;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nullableString(value, label) {
|
|
47
|
+
if (value === null || value === undefined || value === "") return null;
|
|
48
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
49
|
+
throw usageError("E_USAGE_INVALID", `${label} must be a non-empty string or null`);
|
|
50
|
+
}
|
|
51
|
+
return value.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function unknownUsage() {
|
|
55
|
+
return {
|
|
56
|
+
inputTokens: null,
|
|
57
|
+
outputTokens: null,
|
|
58
|
+
cacheReadTokens: null,
|
|
59
|
+
cacheWriteTokens: null,
|
|
60
|
+
totalTokens: null,
|
|
61
|
+
costUsd: null,
|
|
62
|
+
model: null,
|
|
63
|
+
provider: null,
|
|
64
|
+
source: "UNKNOWN",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeUsage(value = {}, { defaultSource = "UNKNOWN", allowedSources = USAGE_SOURCES } = {}) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
throw usageError("E_USAGE_INVALID", "usage must be an object");
|
|
71
|
+
}
|
|
72
|
+
const source = value.source ?? defaultSource;
|
|
73
|
+
if (!allowedSources.includes(source)) {
|
|
74
|
+
throw usageError("E_USAGE_SOURCE_INVALID", `usage.source must be one of ${allowedSources.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
inputTokens: nullableNonNegativeInteger(value.inputTokens, "inputTokens"),
|
|
78
|
+
outputTokens: nullableNonNegativeInteger(value.outputTokens, "outputTokens"),
|
|
79
|
+
cacheReadTokens: nullableNonNegativeInteger(value.cacheReadTokens, "cacheReadTokens"),
|
|
80
|
+
cacheWriteTokens: nullableNonNegativeInteger(value.cacheWriteTokens, "cacheWriteTokens"),
|
|
81
|
+
totalTokens: nullableNonNegativeInteger(value.totalTokens, "totalTokens"),
|
|
82
|
+
costUsd: nullableCost(value.costUsd),
|
|
83
|
+
model: nullableString(value.model, "model"),
|
|
84
|
+
provider: nullableString(value.provider, "provider"),
|
|
85
|
+
source,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createUsageArtifact({ taskId, usage = {}, recordedAt = new Date().toISOString() } = {}) {
|
|
90
|
+
if (typeof taskId !== "string" || !taskId) throw usageError("E_USAGE_INVALID", "taskId is required for usage telemetry");
|
|
91
|
+
if (typeof recordedAt !== "string" || !recordedAt.trim() || Number.isNaN(Date.parse(recordedAt))) {
|
|
92
|
+
throw usageError("E_USAGE_INVALID", "recordedAt must be a valid timestamp");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
protocolVersion: 1,
|
|
97
|
+
taskId,
|
|
98
|
+
recordedAt,
|
|
99
|
+
usage: normalizeUsage(usage),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function providerUsage(value) {
|
|
104
|
+
if (value === null || value === undefined) return unknownUsage();
|
|
105
|
+
const candidate = value?.usage && typeof value.usage === "object" && !Array.isArray(value.usage)
|
|
106
|
+
? value.usage
|
|
107
|
+
: value;
|
|
108
|
+
const source = candidate?.source ?? "HOST_REPORTED";
|
|
109
|
+
if (!["PROVIDER_REPORTED", "HOST_REPORTED"].includes(source)) {
|
|
110
|
+
throw usageError("E_USAGE_SOURCE_INVALID", "host usage providers may report only PROVIDER_REPORTED or HOST_REPORTED");
|
|
111
|
+
}
|
|
112
|
+
return normalizeUsage(candidate, { defaultSource: source, allowedSources: ["PROVIDER_REPORTED", "HOST_REPORTED"] });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function readTaskUsage(target, packageRoot, taskId) {
|
|
116
|
+
try {
|
|
117
|
+
const artifact = await readJsonArtifact(target, taskArtifactPath(taskId, "usage"), "usage", packageRoot);
|
|
118
|
+
return artifact.value.usage;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code === "ARTIFACT_MISSING") return unknownUsage();
|
|
121
|
+
if (error.code === "E_USAGE_SOURCE_INVALID" || error.code === "E_USAGE_INVALID") throw error;
|
|
122
|
+
error.code = "E_USAGE_INVALID";
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function writeTaskUsage(target, packageRoot, artifact, options = {}) {
|
|
128
|
+
const value = createUsageArtifact(artifact);
|
|
129
|
+
return writeJsonArtifact(
|
|
130
|
+
target,
|
|
131
|
+
taskArtifactPath(value.taskId, "usage"),
|
|
132
|
+
value,
|
|
133
|
+
"usage",
|
|
134
|
+
packageRoot,
|
|
135
|
+
{ ...options, taskId: value.taskId, operation: options.operation ?? "usage-record" },
|
|
136
|
+
);
|
|
137
|
+
}
|
package/src/integration.d.ts
CHANGED
|
@@ -59,8 +59,92 @@ export interface ForgeLoopContext {
|
|
|
59
59
|
authorityContext: Record<string, unknown>;
|
|
60
60
|
verificationExecutionAdapter?: Record<string, unknown>;
|
|
61
61
|
verificationExecutionPolicy?: { requiredIsolation: string };
|
|
62
|
+
usageProvider?: ForgeLoopUsageProvider;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
export interface ForgeLoopUsageProvider {
|
|
66
|
+
getTaskUsage(input: { projectPath: string; taskId: string }): Promise<ForgeLoopUsage | Record<string, unknown>> | ForgeLoopUsage | Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ForgeLoopUsage {
|
|
70
|
+
inputTokens: number | null;
|
|
71
|
+
outputTokens: number | null;
|
|
72
|
+
cacheReadTokens: number | null;
|
|
73
|
+
cacheWriteTokens: number | null;
|
|
74
|
+
totalTokens: number | null;
|
|
75
|
+
costUsd: number | null;
|
|
76
|
+
model: string | null;
|
|
77
|
+
provider: string | null;
|
|
78
|
+
source: "PROVIDER_REPORTED" | "HOST_REPORTED";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ForgeLoopContextUsage {
|
|
82
|
+
source: "HOST_REPORTED" | "UNKNOWN";
|
|
83
|
+
profile: "light" | "balanced" | "full" | null;
|
|
84
|
+
items: {
|
|
85
|
+
taskContext: number | null;
|
|
86
|
+
guides: number | null;
|
|
87
|
+
history: number | null;
|
|
88
|
+
protocolInstructions: number | null;
|
|
89
|
+
repositoryContext: number | null;
|
|
90
|
+
other: number | null;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export declare const EXECUTION_PROFILES: readonly ["light", "balanced", "full"];
|
|
95
|
+
export declare const EXECUTION_PROFILE_REQUESTS: readonly ["auto", "light", "balanced", "full"];
|
|
96
|
+
export declare const LEGACY_EXECUTION_PROFILE: "balanced";
|
|
97
|
+
export declare function projectExecutionProfile(route?: Record<string, unknown> | null): "light" | "balanced" | "full" | null;
|
|
98
|
+
export declare function resolveExecutionProfile(input?: {
|
|
99
|
+
routeInput?: Record<string, unknown>;
|
|
100
|
+
contract?: Record<string, unknown> | null;
|
|
101
|
+
taskDescriptor?: Record<string, unknown> | null;
|
|
102
|
+
configuredProfile?: "auto" | "light" | "balanced" | "full";
|
|
103
|
+
requestedProfile?: "auto" | "light" | "balanced" | "full" | null;
|
|
104
|
+
}): {
|
|
105
|
+
requested: "auto" | "light" | "balanced" | "full";
|
|
106
|
+
floor: "light" | "balanced" | "full";
|
|
107
|
+
resolved: "light" | "balanced" | "full";
|
|
108
|
+
reasons: readonly string[];
|
|
109
|
+
escalated: boolean;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export interface ForgeLoopExecutionProfileContext {
|
|
113
|
+
schemaVersion: 1;
|
|
114
|
+
protocolVersion: 1;
|
|
115
|
+
taskId: string;
|
|
116
|
+
executionProfile: {
|
|
117
|
+
requested: "auto" | "light" | "balanced" | "full";
|
|
118
|
+
floor: "light" | "balanced" | "full";
|
|
119
|
+
resolved: "light" | "balanced" | "full";
|
|
120
|
+
reasons: readonly string[];
|
|
121
|
+
escalated: boolean;
|
|
122
|
+
};
|
|
123
|
+
phase: string;
|
|
124
|
+
nextAction: string | null;
|
|
125
|
+
objective: string | null;
|
|
126
|
+
deliverables: readonly string[];
|
|
127
|
+
constraints: readonly string[];
|
|
128
|
+
selectedGuideIds: readonly string[];
|
|
129
|
+
verificationRequirements: readonly Record<string, unknown>[];
|
|
130
|
+
contextPolicy: Record<string, unknown>;
|
|
131
|
+
contextUsage?: ForgeLoopContextUsage;
|
|
132
|
+
optionalContext: { available: readonly string[]; loaded: readonly string[] };
|
|
133
|
+
invariants: Record<string, boolean>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export declare const PROFILE_CONTEXT_POLICIES: Readonly<Record<string, Record<string, unknown>>>;
|
|
137
|
+
export declare function getExecutionProfileContextPolicy(profile: "light" | "balanced" | "full"): Record<string, unknown>;
|
|
138
|
+
export declare function legacyExecutionProfile(): ForgeLoopExecutionProfileContext["executionProfile"];
|
|
139
|
+
export declare function projectExecutionProfileContext(input: Record<string, unknown>): ForgeLoopExecutionProfileContext;
|
|
140
|
+
export declare function buildExecutionProfileContext(input: {
|
|
141
|
+
target: string;
|
|
142
|
+
packageRoot?: string;
|
|
143
|
+
taskId: string;
|
|
144
|
+
authorityContext?: Record<string, unknown>;
|
|
145
|
+
runtimeContext?: Record<string, unknown>;
|
|
146
|
+
}): Promise<ForgeLoopExecutionProfileContext>;
|
|
147
|
+
|
|
64
148
|
export declare const FORGELOOP_INTEGRATION_API_VERSION: number;
|
|
65
149
|
export declare const FORGELOOP_INTEGRATION_RUNTIME_VERSION: number;
|
|
66
150
|
export declare const CLI_COMMAND_DEFINITIONS: Readonly<Record<string, ForgeLoopCommandDefinition>>;
|
package/src/integration.js
CHANGED
|
@@ -45,6 +45,20 @@ export {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
export { createForgeLoopContext } from "./core/runtime-context.js";
|
|
48
|
+
export {
|
|
49
|
+
PROFILE_CONTEXT_POLICIES,
|
|
50
|
+
buildExecutionProfileContext,
|
|
51
|
+
getExecutionProfileContextPolicy,
|
|
52
|
+
legacyExecutionProfile,
|
|
53
|
+
projectExecutionProfileContext,
|
|
54
|
+
} from "./core/execution-profile-context.js";
|
|
55
|
+
export {
|
|
56
|
+
EXECUTION_PROFILES,
|
|
57
|
+
EXECUTION_PROFILE_REQUESTS,
|
|
58
|
+
LEGACY_EXECUTION_PROFILE,
|
|
59
|
+
projectExecutionProfile,
|
|
60
|
+
resolveExecutionProfile,
|
|
61
|
+
} from "./core/execution-profile.js";
|
|
48
62
|
export {
|
|
49
63
|
E_VERIFICATION_EXECUTION_INVALID,
|
|
50
64
|
E_VERIFICATION_ISOLATION_UNAVAILABLE,
|