@loops-adk/core 0.2.0 → 0.3.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 +27 -2
- package/dist/api.d.ts +14 -3
- package/dist/api.js +2 -2
- package/dist/api.js.map +1 -1
- package/dist/{chunk-WM5QVHM2.js → chunk-3PMVII43.js} +195 -5
- package/dist/chunk-3PMVII43.js.map +1 -0
- package/dist/env/command.d.ts +1 -1
- package/dist/env/docker.d.ts +1 -1
- package/dist/env/sst.d.ts +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/{types-Cv_3ymr9.d.ts → types-CpB03Jj4.d.ts} +137 -1
- package/package.json +2 -1
- package/dist/chunk-WM5QVHM2.js.map +0 -1
|
@@ -4,9 +4,9 @@ import { isLimitError, waitMsFor } from './chunk-Y2SD7GBL.js';
|
|
|
4
4
|
import { LoopError } from './chunk-I3STY7U6.js';
|
|
5
5
|
import { readFileSync, mkdtempSync, existsSync, writeFileSync, appendFileSync, readdirSync, mkdirSync, rmSync } from 'fs';
|
|
6
6
|
import { execa } from 'execa';
|
|
7
|
+
import { createHash, randomBytes } from 'crypto';
|
|
7
8
|
import { tmpdir, homedir } from 'os';
|
|
8
9
|
import { join, dirname } from 'path';
|
|
9
|
-
import { randomBytes } from 'crypto';
|
|
10
10
|
|
|
11
11
|
// src/core/describe.ts
|
|
12
12
|
var META = /* @__PURE__ */ new WeakMap();
|
|
@@ -54,7 +54,11 @@ function renderPlan(meta, indent = "") {
|
|
|
54
54
|
const out = [];
|
|
55
55
|
switch (meta.kind) {
|
|
56
56
|
case "loop": {
|
|
57
|
-
const
|
|
57
|
+
const caps = [];
|
|
58
|
+
if (typeof meta.max === "number") caps.push(`max ${meta.max}`);
|
|
59
|
+
if (typeof meta.noProgress === "number")
|
|
60
|
+
caps.push(`stall after ${meta.noProgress} flat`);
|
|
61
|
+
const max = caps.length ? ` (${caps.join("; ")})` : "";
|
|
58
62
|
out.push(`${indent}loop${nm}${max}`);
|
|
59
63
|
const start = meta.start;
|
|
60
64
|
const gate = meta.gate;
|
|
@@ -812,6 +816,32 @@ async function isDirty(opts) {
|
|
|
812
816
|
const r = await git(["status", "--porcelain"], opts);
|
|
813
817
|
return r.stdout.trim().length > 0;
|
|
814
818
|
}
|
|
819
|
+
async function workspaceFingerprint(opts) {
|
|
820
|
+
try {
|
|
821
|
+
if (!await isRepo(opts)) return void 0;
|
|
822
|
+
const hash = createHash("sha256");
|
|
823
|
+
const feed = (label, value) => {
|
|
824
|
+
hash.update(label);
|
|
825
|
+
hash.update("");
|
|
826
|
+
hash.update(value);
|
|
827
|
+
hash.update("");
|
|
828
|
+
};
|
|
829
|
+
feed("head", await headSha(opts) ?? "");
|
|
830
|
+
feed("status", (await git(["status", "--porcelain"], opts)).stdout);
|
|
831
|
+
feed("unstaged", (await git(["diff"], opts)).stdout);
|
|
832
|
+
feed("staged", (await git(["diff", "--cached"], opts)).stdout);
|
|
833
|
+
const untracked = (await git(["ls-files", "--others", "--exclude-standard"], opts)).stdout.split("\n").filter(Boolean);
|
|
834
|
+
for (let i = 0; i < untracked.length; i += 500) {
|
|
835
|
+
const chunk = untracked.slice(i, i + 500);
|
|
836
|
+
feed(`untracked-paths:${i}`, chunk.join("\n"));
|
|
837
|
+
const r = await git(["hash-object", "--", ...chunk], opts);
|
|
838
|
+
if (r.exitCode === 0) feed(`untracked-content:${i}`, r.stdout);
|
|
839
|
+
}
|
|
840
|
+
return hash.digest("hex");
|
|
841
|
+
} catch {
|
|
842
|
+
return void 0;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
815
845
|
async function commit(input, opts) {
|
|
816
846
|
if (!input.allowEmpty && !await hasStagedChanges(opts)) return void 0;
|
|
817
847
|
const message = input.body ? `${input.subject}
|
|
@@ -1780,6 +1810,102 @@ function fnJob(label, fn) {
|
|
|
1780
1810
|
return setMeta(job, { kind: "fn", name: label });
|
|
1781
1811
|
}
|
|
1782
1812
|
|
|
1813
|
+
// src/core/progress.ts
|
|
1814
|
+
function resolveNoProgress(input) {
|
|
1815
|
+
if (input == null) return void 0;
|
|
1816
|
+
const cfg = typeof input === "number" ? { window: input } : input;
|
|
1817
|
+
return {
|
|
1818
|
+
...cfg,
|
|
1819
|
+
window: cfg.window ?? 3,
|
|
1820
|
+
minConfidenceDelta: cfg.minConfidenceDelta ?? 0.02
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
var ProgressTracker = class {
|
|
1824
|
+
window;
|
|
1825
|
+
minConfidenceDelta;
|
|
1826
|
+
/** Every state this run has reached, namespaced by channel. */
|
|
1827
|
+
seen = /* @__PURE__ */ new Set();
|
|
1828
|
+
/** Confidence high-water mark — the best score at the last progress point. */
|
|
1829
|
+
best;
|
|
1830
|
+
/** The current run of consecutive no-progress iterations. */
|
|
1831
|
+
stalledRun = [];
|
|
1832
|
+
lastEvidence = [];
|
|
1833
|
+
lastReason = "gate not met";
|
|
1834
|
+
indeterminate = 0;
|
|
1835
|
+
sampled = 0;
|
|
1836
|
+
constructor(cfg) {
|
|
1837
|
+
this.window = cfg.window;
|
|
1838
|
+
this.minConfidenceDelta = cfg.minConfidenceDelta;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Record one iteration. Returns a `StallReport` when this sample fills the
|
|
1842
|
+
* window, else undefined.
|
|
1843
|
+
*/
|
|
1844
|
+
record(sample) {
|
|
1845
|
+
this.sampled += 1;
|
|
1846
|
+
if (sample.reason) this.lastReason = sample.reason;
|
|
1847
|
+
const flat = [];
|
|
1848
|
+
let progressed = false;
|
|
1849
|
+
let channels = 0;
|
|
1850
|
+
if (sample.fingerprint !== void 0) {
|
|
1851
|
+
channels += 1;
|
|
1852
|
+
const key = `fp:${sample.fingerprint}`;
|
|
1853
|
+
if (this.seen.has(key)) {
|
|
1854
|
+
flat.push("workspace: no state this run has not already visited");
|
|
1855
|
+
} else {
|
|
1856
|
+
this.seen.add(key);
|
|
1857
|
+
progressed = true;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (sample.signal !== void 0) {
|
|
1861
|
+
channels += 1;
|
|
1862
|
+
const key = `sig:${sample.signal}`;
|
|
1863
|
+
if (this.seen.has(key)) {
|
|
1864
|
+
flat.push(`signal: "${sample.signal}" already seen this run`);
|
|
1865
|
+
} else {
|
|
1866
|
+
this.seen.add(key);
|
|
1867
|
+
progressed = true;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (sample.confidence !== void 0) {
|
|
1871
|
+
channels += 1;
|
|
1872
|
+
if (this.best === void 0 || sample.confidence >= this.best + this.minConfidenceDelta) {
|
|
1873
|
+
this.best = Math.max(this.best ?? -Infinity, sample.confidence);
|
|
1874
|
+
progressed = true;
|
|
1875
|
+
} else {
|
|
1876
|
+
flat.push(
|
|
1877
|
+
`confidence ${sample.confidence.toFixed(2)} did not improve on ${this.best.toFixed(2)} (needs +${this.minConfidenceDelta})`
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (channels === 0) {
|
|
1882
|
+
this.indeterminate += 1;
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
if (progressed) {
|
|
1886
|
+
this.stalledRun = [];
|
|
1887
|
+
return void 0;
|
|
1888
|
+
}
|
|
1889
|
+
this.stalledRun.push(sample.iteration);
|
|
1890
|
+
this.lastEvidence = flat;
|
|
1891
|
+
if (this.stalledRun.length < this.window) return void 0;
|
|
1892
|
+
return {
|
|
1893
|
+
window: this.window,
|
|
1894
|
+
iterations: [...this.stalledRun],
|
|
1895
|
+
reason: this.lastReason,
|
|
1896
|
+
evidence: [...this.lastEvidence]
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* True when the detector has seen a full window of samples and none carried
|
|
1901
|
+
* any evidence channel — detection is configured but cannot fire. The loop
|
|
1902
|
+
* uses this to warn once instead of failing silently-inert.
|
|
1903
|
+
*/
|
|
1904
|
+
isInert() {
|
|
1905
|
+
return this.indeterminate >= this.window && this.indeterminate === this.sampled;
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1783
1909
|
// src/core/context.ts
|
|
1784
1910
|
function childContext(parent, over) {
|
|
1785
1911
|
return {
|
|
@@ -1842,6 +1968,7 @@ function loop(config) {
|
|
|
1842
1968
|
const until = config.until ? toCondition(config.until) : void 0;
|
|
1843
1969
|
const stopOn = config.stopOn ? toCondition(config.stopOn) : void 0;
|
|
1844
1970
|
const onError = config.retry?.onError ?? "continue";
|
|
1971
|
+
const noProgress = resolveNoProgress(config.noProgress);
|
|
1845
1972
|
const job = async (parent) => {
|
|
1846
1973
|
const path = [...parent.path, config.name];
|
|
1847
1974
|
const depth = parent.depth + 1;
|
|
@@ -1928,6 +2055,8 @@ function loop(config) {
|
|
|
1928
2055
|
let last;
|
|
1929
2056
|
let consecutiveErrors = 0;
|
|
1930
2057
|
let consecutiveReviewFails = 0;
|
|
2058
|
+
const tracker = noProgress ? new ProgressTracker(noProgress) : void 0;
|
|
2059
|
+
let warnedInert = false;
|
|
1931
2060
|
while (true) {
|
|
1932
2061
|
await yieldToLoop();
|
|
1933
2062
|
if (parent.signal.aborted)
|
|
@@ -1948,6 +2077,7 @@ function loop(config) {
|
|
|
1948
2077
|
}
|
|
1949
2078
|
iteration += 1;
|
|
1950
2079
|
const ctx = ctxAt(iteration, last);
|
|
2080
|
+
let turnReview;
|
|
1951
2081
|
parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
|
|
1952
2082
|
let bodyThrew = false;
|
|
1953
2083
|
try {
|
|
@@ -2133,6 +2263,7 @@ function loop(config) {
|
|
|
2133
2263
|
}
|
|
2134
2264
|
consecutiveReviewFails += 1;
|
|
2135
2265
|
lastReview = reviewOutcome;
|
|
2266
|
+
turnReview = reviewOutcome;
|
|
2136
2267
|
parent.log(
|
|
2137
2268
|
`review did not pass (${reviewOutcome.summary ?? reviewOutcome.status}); re-entering ${config.name}`,
|
|
2138
2269
|
"warn"
|
|
@@ -2148,6 +2279,62 @@ function loop(config) {
|
|
|
2148
2279
|
);
|
|
2149
2280
|
}
|
|
2150
2281
|
}
|
|
2282
|
+
if (tracker) {
|
|
2283
|
+
let fingerprint;
|
|
2284
|
+
if (noProgress.workspace !== false) {
|
|
2285
|
+
fingerprint = await workspaceFingerprint({
|
|
2286
|
+
cwd: ctx.workspace.dir,
|
|
2287
|
+
signal: parent.signal
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
let signalValue;
|
|
2291
|
+
if (noProgress.signal) {
|
|
2292
|
+
try {
|
|
2293
|
+
const v = await noProgress.signal(ctx, last);
|
|
2294
|
+
signalValue = v == null ? void 0 : String(v);
|
|
2295
|
+
} catch (e) {
|
|
2296
|
+
throw LoopError.from(e, {
|
|
2297
|
+
code: "VALIDATION",
|
|
2298
|
+
phase: "body",
|
|
2299
|
+
path,
|
|
2300
|
+
iteration
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
const report = tracker.record({
|
|
2305
|
+
iteration,
|
|
2306
|
+
fingerprint,
|
|
2307
|
+
signal: signalValue,
|
|
2308
|
+
confidence: turnReview?.confidence ?? conv.confidence ?? last.confidence,
|
|
2309
|
+
reason: turnReview ? turnReview.summary ?? "review rejected" : conv.reason
|
|
2310
|
+
});
|
|
2311
|
+
if (!warnedInert && tracker.isInert()) {
|
|
2312
|
+
warnedInert = true;
|
|
2313
|
+
parent.log(
|
|
2314
|
+
`noProgress is set on ${config.name} but no evidence channel exists (no git workspace, no gate confidence, no custom signal); stall detection is inert`,
|
|
2315
|
+
"warn"
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
if (report) {
|
|
2319
|
+
parent.emit({
|
|
2320
|
+
kind: "loop:stall",
|
|
2321
|
+
ts: ts(),
|
|
2322
|
+
path,
|
|
2323
|
+
iteration,
|
|
2324
|
+
report
|
|
2325
|
+
});
|
|
2326
|
+
return finish(
|
|
2327
|
+
{
|
|
2328
|
+
status: "exhausted",
|
|
2329
|
+
summary: `stalled after ${report.iterations.length} iterations with no observable progress: ${report.reason}`,
|
|
2330
|
+
confidence: last.confidence,
|
|
2331
|
+
data: last.data,
|
|
2332
|
+
stall: report
|
|
2333
|
+
},
|
|
2334
|
+
iteration
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2151
2338
|
if (config.delayMs) await delay(config.delayMs, parent.signal);
|
|
2152
2339
|
}
|
|
2153
2340
|
} catch (e) {
|
|
@@ -2169,6 +2356,7 @@ function loop(config) {
|
|
|
2169
2356
|
kind: "loop",
|
|
2170
2357
|
name: config.name,
|
|
2171
2358
|
max: config.max,
|
|
2359
|
+
noProgress: noProgress?.window,
|
|
2172
2360
|
start: describeConditions(config.start),
|
|
2173
2361
|
gate: describeConditions(config.until),
|
|
2174
2362
|
stopOn: describeConditions(config.stopOn),
|
|
@@ -2685,6 +2873,8 @@ function formatEvent(event) {
|
|
|
2685
2873
|
return `${at} tool ${event.name} ${event.phase}`;
|
|
2686
2874
|
case "engine:usage":
|
|
2687
2875
|
return `${at} ${event.model}: ${event.usage.inputTokens}/${event.usage.outputTokens} tok`;
|
|
2876
|
+
case "loop:stall":
|
|
2877
|
+
return `${at}\u23F9 stalled after ${event.report.iterations.length} no-progress iterations: ${event.report.reason}`;
|
|
2688
2878
|
case "limit:wait":
|
|
2689
2879
|
return `${at}\u23F8 limit ${event.code}: waiting ${Math.round(event.waitMs / 1e3)}s`;
|
|
2690
2880
|
case "limit:pause":
|
|
@@ -2901,6 +3091,6 @@ function exitCodeFor(outcome) {
|
|
|
2901
3091
|
}
|
|
2902
3092
|
}
|
|
2903
3093
|
|
|
2904
|
-
export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentContract, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, feedbackBlock, fnJob, forgeChecks, formatEvent, fromFile, gateJob, graphPositionBlock, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, isRequiredFeedbackSeverity, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, normalizeFeedbackSeverity, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, reviewContext, reviewPanel, revisionFromOutcome, revisionRequest, run, runEventsPath, runSemanticRecordsPath, runsHome, semanticRecordsFromEvent, setMeta, stageAll, toCondition };
|
|
2905
|
-
//# sourceMappingURL=chunk-
|
|
2906
|
-
//# sourceMappingURL=chunk-
|
|
3094
|
+
export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, ProgressTracker, Stats, addWorktree, agentCheck, agentContract, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, feedbackBlock, fnJob, forgeChecks, formatEvent, fromFile, gateJob, graphPositionBlock, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, isRequiredFeedbackSeverity, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, normalizeFeedbackSeverity, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveNoProgress, resolveSystem, retrieveLedger, reviewContext, reviewPanel, revisionFromOutcome, revisionRequest, run, runEventsPath, runSemanticRecordsPath, runsHome, semanticRecordsFromEvent, setMeta, stageAll, toCondition, workspaceFingerprint };
|
|
3095
|
+
//# sourceMappingURL=chunk-3PMVII43.js.map
|
|
3096
|
+
//# sourceMappingURL=chunk-3PMVII43.js.map
|