dsh-agy-link 0.4.49 → 0.4.53
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/CHANGELOG.md +60 -0
- package/dist/client.js +156 -86
- package/dist/index.js +573 -76
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -457,6 +457,7 @@ const AUTH_REVERIFY_DELAY_MS = 6e5;
|
|
|
457
457
|
* simply re-stamped.
|
|
458
458
|
*/
|
|
459
459
|
function needsAuthReverify(account, now = Date.now()) {
|
|
460
|
+
if (!account.enabled) return false;
|
|
460
461
|
if (!account.authRequired) return false;
|
|
461
462
|
if (typeof account.authMarkedAt !== "number") return true;
|
|
462
463
|
return now - account.authMarkedAt > AUTH_REVERIFY_DELAY_MS;
|
|
@@ -1348,6 +1349,158 @@ function defineAgyMirrorTool(deps) {
|
|
|
1348
1349
|
});
|
|
1349
1350
|
}
|
|
1350
1351
|
//#endregion
|
|
1352
|
+
//#region src/host/dsh-goal.ts
|
|
1353
|
+
const toToolCallId$1 = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
|
|
1354
|
+
function extractText$1(m) {
|
|
1355
|
+
const content = m.content;
|
|
1356
|
+
if (typeof content === "string") return content;
|
|
1357
|
+
if (Array.isArray(content)) {
|
|
1358
|
+
const parts = [];
|
|
1359
|
+
for (const b of content) if (b && typeof b === "object") {
|
|
1360
|
+
const blk = b;
|
|
1361
|
+
if (blk.type === "text" && typeof blk.text === "string") parts.push(blk.text);
|
|
1362
|
+
}
|
|
1363
|
+
return parts.join("\n");
|
|
1364
|
+
}
|
|
1365
|
+
return "";
|
|
1366
|
+
}
|
|
1367
|
+
function parseGoalRoundText(text) {
|
|
1368
|
+
if (!text.includes("<goal_round>")) return null;
|
|
1369
|
+
const roundMatch = text.match(/Round:\s*(\d+)(?:\/(\d+))?/);
|
|
1370
|
+
const round = roundMatch ? parseInt(roundMatch[1], 10) : void 0;
|
|
1371
|
+
const maxRounds = roundMatch && roundMatch[2] ? parseInt(roundMatch[2], 10) : void 0;
|
|
1372
|
+
const objMatch = text.match(/Objective:\s*(?:"([^"]*)"|'([^']*)'|([^\n]+))/);
|
|
1373
|
+
const objective = objMatch ? (objMatch[1] || objMatch[2] || objMatch[3])?.trim() : void 0;
|
|
1374
|
+
const idMatch = text.match(/goal-[a-zA-Z0-9_-]+/);
|
|
1375
|
+
return {
|
|
1376
|
+
goalId: idMatch ? idMatch[0] : void 0,
|
|
1377
|
+
round,
|
|
1378
|
+
maxRounds,
|
|
1379
|
+
objective
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Detect whether the incoming request is part of a DSH <goal_round> continuation.
|
|
1384
|
+
* Extracts goalId, revision, round, and objective if available.
|
|
1385
|
+
*/
|
|
1386
|
+
function detectGoalContext(messages) {
|
|
1387
|
+
let goalId;
|
|
1388
|
+
let revision;
|
|
1389
|
+
let round;
|
|
1390
|
+
let maxRounds;
|
|
1391
|
+
let objective;
|
|
1392
|
+
let isGoal = false;
|
|
1393
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1394
|
+
const m = messages[i];
|
|
1395
|
+
if (!m) continue;
|
|
1396
|
+
const src = m.source;
|
|
1397
|
+
if (src && typeof src === "object" && src.kind === "goal") {
|
|
1398
|
+
isGoal = true;
|
|
1399
|
+
if (!goalId && typeof src.goalId === "string") goalId = src.goalId;
|
|
1400
|
+
if (revision === void 0 && typeof src.revision === "number") revision = src.revision;
|
|
1401
|
+
if (round === void 0 && typeof src.round === "number") round = src.round;
|
|
1402
|
+
}
|
|
1403
|
+
const text = extractText$1(m);
|
|
1404
|
+
if (text.includes("<goal_round>")) {
|
|
1405
|
+
isGoal = true;
|
|
1406
|
+
const parsed = parseGoalRoundText(text);
|
|
1407
|
+
if (parsed) {
|
|
1408
|
+
if (!goalId && parsed.goalId) goalId = parsed.goalId;
|
|
1409
|
+
if (revision === void 0 && parsed.revision !== void 0) revision = parsed.revision;
|
|
1410
|
+
if (round === void 0 && parsed.round !== void 0) round = parsed.round;
|
|
1411
|
+
if (maxRounds === void 0 && parsed.maxRounds !== void 0) maxRounds = parsed.maxRounds;
|
|
1412
|
+
if (!objective && parsed.objective) objective = parsed.objective;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (isGoal) return {
|
|
1417
|
+
goalId,
|
|
1418
|
+
revision: revision ?? 1,
|
|
1419
|
+
round,
|
|
1420
|
+
maxRounds,
|
|
1421
|
+
objective
|
|
1422
|
+
};
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Detect whether DSH is in Plan Mode.
|
|
1427
|
+
* DSH plan-mode injects "You are in plan mode" into the runtime context prompt.
|
|
1428
|
+
*/
|
|
1429
|
+
function isPlanMode(messages) {
|
|
1430
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1431
|
+
const m = messages[i];
|
|
1432
|
+
if (!m) continue;
|
|
1433
|
+
if (extractText$1(m).includes("You are in plan mode")) return true;
|
|
1434
|
+
}
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Check if the trailing message is the tool result of a bridge-emitted `update_goal` call.
|
|
1439
|
+
*/
|
|
1440
|
+
function isGoalToolContinuation(messages) {
|
|
1441
|
+
if (messages.length === 0) return false;
|
|
1442
|
+
const last = messages[messages.length - 1];
|
|
1443
|
+
if (!last) return false;
|
|
1444
|
+
const callId = last.toolCallId ?? last.source?.callId;
|
|
1445
|
+
if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Check if the trailing message is the tool result of a bridge-emitted `exit_plan_mode` call.
|
|
1450
|
+
*/
|
|
1451
|
+
function isPlanToolContinuation(messages) {
|
|
1452
|
+
if (messages.length === 0) return false;
|
|
1453
|
+
const last = messages[messages.length - 1];
|
|
1454
|
+
if (!last) return false;
|
|
1455
|
+
const callId = last.toolCallId ?? last.source?.callId;
|
|
1456
|
+
if (typeof callId === "string" && callId.startsWith("agytc-bridge-plan-")) return true;
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Build the synthetic `update_goal` tool-call chunk that marks the goal complete on DSH.
|
|
1461
|
+
*/
|
|
1462
|
+
function buildGoalCompleteToolCallBlock(goalContext, blockIdx) {
|
|
1463
|
+
const goalId = goalContext.goalId || "active-goal";
|
|
1464
|
+
const revision = goalContext.revision ?? 1;
|
|
1465
|
+
return [{
|
|
1466
|
+
type: "block-start",
|
|
1467
|
+
index: blockIdx,
|
|
1468
|
+
blockType: "tool-call"
|
|
1469
|
+
}, {
|
|
1470
|
+
type: "block-end",
|
|
1471
|
+
index: blockIdx,
|
|
1472
|
+
block: {
|
|
1473
|
+
type: "tool-call",
|
|
1474
|
+
id: toToolCallId$1(`agytc-bridge-goal-${goalId}-${revision}`),
|
|
1475
|
+
name: "update_goal",
|
|
1476
|
+
arguments: JSON.stringify({
|
|
1477
|
+
goal_id: goalId,
|
|
1478
|
+
revision,
|
|
1479
|
+
action: "complete"
|
|
1480
|
+
})
|
|
1481
|
+
}
|
|
1482
|
+
}];
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Build the synthetic `exit_plan_mode` tool-call chunk that presents the generated plan to DSH.
|
|
1486
|
+
*/
|
|
1487
|
+
function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
|
|
1488
|
+
return [{
|
|
1489
|
+
type: "block-start",
|
|
1490
|
+
index: blockIdx,
|
|
1491
|
+
blockType: "tool-call"
|
|
1492
|
+
}, {
|
|
1493
|
+
type: "block-end",
|
|
1494
|
+
index: blockIdx,
|
|
1495
|
+
block: {
|
|
1496
|
+
type: "tool-call",
|
|
1497
|
+
id: toToolCallId$1(`agytc-bridge-plan-${Date.now()}`),
|
|
1498
|
+
name: "exit_plan_mode",
|
|
1499
|
+
arguments: JSON.stringify({ plan: planMarkdown.trim() !== "" ? planMarkdown : "# Plan Completed\n\nPlan generated and ready for review." })
|
|
1500
|
+
}
|
|
1501
|
+
}];
|
|
1502
|
+
}
|
|
1503
|
+
//#endregion
|
|
1351
1504
|
//#region src/host/mapper.ts
|
|
1352
1505
|
const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
|
|
1353
1506
|
function usageFromRaw(raw) {
|
|
@@ -1396,6 +1549,7 @@ var EventMapper = class {
|
|
|
1396
1549
|
bannerOnlyThinkingEmitted = false;
|
|
1397
1550
|
sawTextStep;
|
|
1398
1551
|
finished = false;
|
|
1552
|
+
totalTextOutput = "";
|
|
1399
1553
|
constructor(opts) {
|
|
1400
1554
|
this.opts = opts;
|
|
1401
1555
|
this.sawTextStep = opts.initialSawText === true;
|
|
@@ -1438,6 +1592,7 @@ var EventMapper = class {
|
|
|
1438
1592
|
appendDelta(delta) {
|
|
1439
1593
|
if (delta === "") return null;
|
|
1440
1594
|
this.openAcc += delta;
|
|
1595
|
+
if (this.openType === "text") this.totalTextOutput += delta;
|
|
1441
1596
|
return this.openType === "text" ? {
|
|
1442
1597
|
type: "text-delta",
|
|
1443
1598
|
index: this.blockIdx,
|
|
@@ -1449,10 +1604,10 @@ var EventMapper = class {
|
|
|
1449
1604
|
};
|
|
1450
1605
|
}
|
|
1451
1606
|
/**
|
|
1452
|
-
* Emit thinking: always retain the [agy
|
|
1453
|
-
*
|
|
1607
|
+
* Emit thinking: always retain the [agy · ... tokens] banner.
|
|
1608
|
+
* When real thought prose is present, place the banner and thought
|
|
1454
1609
|
* text on the same initial line so DSH's collapsed summary renders:
|
|
1455
|
-
* [agy
|
|
1610
|
+
* [agy · *** tokens] [Chain-of-Thought body]
|
|
1456
1611
|
*
|
|
1457
1612
|
* With `allowBannerOnly` the bare banner is emitted when the DB prose never
|
|
1458
1613
|
* arrived (desktop hosts without any sqlite reader, issue #43): the user
|
|
@@ -1466,7 +1621,7 @@ var EventMapper = class {
|
|
|
1466
1621
|
const bannerAlreadyShown = this.bannerOnlyShown.has(stepKey);
|
|
1467
1622
|
if (hasText) this.bannerOnlyShown.delete(stepKey);
|
|
1468
1623
|
yield* this.ensureBlock("reasoning");
|
|
1469
|
-
const banner = thoughtTokens > 0 ? "[agy
|
|
1624
|
+
const banner = thoughtTokens > 0 ? "[agy · " + thoughtTokens + " tokens]" : "[agy]";
|
|
1470
1625
|
const combined = !hasText ? `${banner}\n` : bannerAlreadyShown ? `${text.trim()}\n` : `${banner} ${text.trim()}\n`;
|
|
1471
1626
|
const d = this.appendDelta(combined);
|
|
1472
1627
|
if (d) yield d;
|
|
@@ -1650,6 +1805,28 @@ var EventMapper = class {
|
|
|
1650
1805
|
type: "usage",
|
|
1651
1806
|
usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
|
|
1652
1807
|
};
|
|
1808
|
+
if (this.opts.supportsGoalUpdate && this.opts.goalContext?.goalId) {
|
|
1809
|
+
for (const ch of buildGoalCompleteToolCallBlock(this.opts.goalContext, this.blockIdx)) yield ch;
|
|
1810
|
+
this.blockIdx++;
|
|
1811
|
+
yield {
|
|
1812
|
+
type: "finish",
|
|
1813
|
+
reason: { kind: "tool-calls" },
|
|
1814
|
+
...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
|
|
1815
|
+
};
|
|
1816
|
+
this.finished = true;
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (this.opts.supportsPlanExit && this.totalTextOutput.trim() !== "") {
|
|
1820
|
+
for (const ch of buildPlanExitToolCallBlock(this.totalTextOutput, this.blockIdx)) yield ch;
|
|
1821
|
+
this.blockIdx++;
|
|
1822
|
+
yield {
|
|
1823
|
+
type: "finish",
|
|
1824
|
+
reason: { kind: "tool-calls" },
|
|
1825
|
+
...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
|
|
1826
|
+
};
|
|
1827
|
+
this.finished = true;
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1653
1830
|
yield {
|
|
1654
1831
|
type: "finish",
|
|
1655
1832
|
reason: { kind: "stop" },
|
|
@@ -2291,24 +2468,25 @@ var StreamJsonParser = class {
|
|
|
2291
2468
|
const ev = this.takeLine(rest);
|
|
2292
2469
|
return ev ? [ev] : [];
|
|
2293
2470
|
}
|
|
2471
|
+
recordAuthFailure(text) {
|
|
2472
|
+
this.stats.sawAuthFailure = true;
|
|
2473
|
+
const prev = this.stats.authFailureText ?? "";
|
|
2474
|
+
if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + text.trim().slice(0, 300);
|
|
2475
|
+
}
|
|
2294
2476
|
takeLine(line) {
|
|
2295
2477
|
if (line.trim() === "") return void 0;
|
|
2296
2478
|
this.stats.lines++;
|
|
2297
2479
|
this.recentLines.push(line);
|
|
2298
2480
|
if (this.recentLines.length > this.maxRecent) this.recentLines.splice(0, this.recentLines.length - this.maxRecent);
|
|
2299
|
-
if (looksLikeAuthFailure(line)) {
|
|
2300
|
-
this.stats.sawAuthFailure = true;
|
|
2301
|
-
const prev = this.stats.authFailureText ?? "";
|
|
2302
|
-
if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + line.trim().slice(0, 300);
|
|
2303
|
-
}
|
|
2304
|
-
if (this.stats.authUrl === void 0) {
|
|
2305
|
-
const u = extractAuthUrl(line);
|
|
2306
|
-
if (u) this.stats.authUrl = u;
|
|
2307
|
-
}
|
|
2308
2481
|
let obj;
|
|
2309
2482
|
try {
|
|
2310
2483
|
obj = JSON.parse(line);
|
|
2311
2484
|
} catch {
|
|
2485
|
+
if (looksLikeAuthFailure(line)) this.recordAuthFailure(line);
|
|
2486
|
+
if (this.stats.authUrl === void 0) {
|
|
2487
|
+
const u = extractAuthUrl(line);
|
|
2488
|
+
if (u) this.stats.authUrl = u;
|
|
2489
|
+
}
|
|
2312
2490
|
this.stats.garbage++;
|
|
2313
2491
|
this.stats.consecutiveGarbage++;
|
|
2314
2492
|
return {
|
|
@@ -2325,7 +2503,10 @@ var StreamJsonParser = class {
|
|
|
2325
2503
|
line
|
|
2326
2504
|
};
|
|
2327
2505
|
}
|
|
2328
|
-
if (ev.kind === "result")
|
|
2506
|
+
if (ev.kind === "result") {
|
|
2507
|
+
this.stats.lastResultError = ev.error ?? void 0;
|
|
2508
|
+
if (!ev.ok && ev.error && looksLikeAuthFailure(ev.error)) this.recordAuthFailure(ev.error);
|
|
2509
|
+
}
|
|
2329
2510
|
this.stats.consecutiveGarbage = 0;
|
|
2330
2511
|
return ev;
|
|
2331
2512
|
}
|
|
@@ -2735,6 +2916,20 @@ function buildStreamInputLine(prompt) {
|
|
|
2735
2916
|
}
|
|
2736
2917
|
}) + "\n";
|
|
2737
2918
|
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Remove NUL characters from text destined for a child process.
|
|
2921
|
+
*
|
|
2922
|
+
* `spawn()` rejects any argv element containing U+0000 with
|
|
2923
|
+
* `ERR_INVALID_ARG_VALUE: The argument 'args[N]' must be a string without null
|
|
2924
|
+
* bytes`, which aborts the whole turn before agy is even started. NULs reach the
|
|
2925
|
+
* assembled prompt through binary tool output (a tool result that quoted a
|
|
2926
|
+
* binary file — `.git` objects, packed refs, images, …), so every prompt that
|
|
2927
|
+
* can carry tool text passes through here. Other control characters are legal in
|
|
2928
|
+
* argv and are left untouched.
|
|
2929
|
+
*/
|
|
2930
|
+
function stripNulBytes(text) {
|
|
2931
|
+
return text.includes("\0") ? text.split("\0").join("") : text;
|
|
2932
|
+
}
|
|
2738
2933
|
/** Whether the assembled argv (including a trailing `-p <prompt>`) risks ENAMETOOLONG. */
|
|
2739
2934
|
function shouldUsePromptStdin(argsWithPrompt) {
|
|
2740
2935
|
let size = 0;
|
|
@@ -3497,6 +3692,21 @@ var AccountPoolManager = class {
|
|
|
3497
3692
|
this.data.lastActiveAccountId = id;
|
|
3498
3693
|
this.persist();
|
|
3499
3694
|
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Manually pin the active slot for one family (console "设为当前使用").
|
|
3697
|
+
* The pin survives until the scheduler legitimately rotates away (quota
|
|
3698
|
+
* exhaustion / cooldown / auth flag) — getActiveAccountId still enforces
|
|
3699
|
+
* eligibility, so pinning an ineligible slot simply does not stick.
|
|
3700
|
+
*/
|
|
3701
|
+
setActiveAccount(id, family = "google") {
|
|
3702
|
+
const acc = this.getAccount(id);
|
|
3703
|
+
if (!acc || !acc.enabled || acc.authRequired) return false;
|
|
3704
|
+
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
3705
|
+
this.data.activeAccountIds[family] = id;
|
|
3706
|
+
this.data.lastActiveAccountId = id;
|
|
3707
|
+
this.persist();
|
|
3708
|
+
return true;
|
|
3709
|
+
}
|
|
3500
3710
|
markAuthRequired(id, reason) {
|
|
3501
3711
|
const acc = this.getAccount(id);
|
|
3502
3712
|
if (!acc) return;
|
|
@@ -3548,9 +3758,11 @@ var AccountPoolManager = class {
|
|
|
3548
3758
|
setPrimaryAccount(id) {
|
|
3549
3759
|
const idx = this.data.accounts.findIndex((a) => a.id === id);
|
|
3550
3760
|
if (idx === -1) return false;
|
|
3551
|
-
this.data.primaryAccountId = id;
|
|
3552
3761
|
const [acc] = this.data.accounts.splice(idx, 1);
|
|
3553
|
-
if (acc)
|
|
3762
|
+
if (!acc) return false;
|
|
3763
|
+
acc.enabled = true;
|
|
3764
|
+
this.data.primaryAccountId = id;
|
|
3765
|
+
this.data.accounts.unshift(acc);
|
|
3554
3766
|
this.data.activeAccountIds = {
|
|
3555
3767
|
google: id,
|
|
3556
3768
|
anthropic: id,
|
|
@@ -3598,7 +3810,7 @@ var AccountPoolManager = class {
|
|
|
3598
3810
|
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
|
|
3599
3811
|
if (quota.weeklyResetTime) {
|
|
3600
3812
|
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
3601
|
-
if (
|
|
3813
|
+
if (Number.isNaN(resetMs) || resetMs > now) return false;
|
|
3602
3814
|
} else return false;
|
|
3603
3815
|
}
|
|
3604
3816
|
return true;
|
|
@@ -3617,7 +3829,7 @@ var AccountPoolManager = class {
|
|
|
3617
3829
|
}
|
|
3618
3830
|
const next = this.selectAccount(family);
|
|
3619
3831
|
if (next) return next.id;
|
|
3620
|
-
return this.data.primaryAccountId ?? this.data.accounts
|
|
3832
|
+
return this.data.accounts.find((a) => a.id === this.data.primaryAccountId && a.enabled)?.id ?? this.data.accounts.find((a) => a.enabled)?.id;
|
|
3621
3833
|
}
|
|
3622
3834
|
/**
|
|
3623
3835
|
* Next moment this account's quota state refreshes for one family: the
|
|
@@ -3639,23 +3851,34 @@ var AccountPoolManager = class {
|
|
|
3639
3851
|
return (idx - start + total) % total;
|
|
3640
3852
|
}
|
|
3641
3853
|
/**
|
|
3642
|
-
* "Use it or lose it" rotation order.
|
|
3643
|
-
*
|
|
3644
|
-
*
|
|
3645
|
-
*
|
|
3646
|
-
* the legacy cyclic order (which also keeps a drained slot from bouncing
|
|
3647
|
-
* straight back to the primary).
|
|
3854
|
+
* "Use it or lose it" rotation order.
|
|
3855
|
+
* Priority 1: Weekly quota expiring soonest (least remaining time until weeklyResetTime).
|
|
3856
|
+
* Priority 2: 5-hour quota expiring soonest (least remaining time until resetTime).
|
|
3857
|
+
* Priority 3: Cyclic distance from `fromId` (or pool order).
|
|
3648
3858
|
*/
|
|
3649
3859
|
orderByExpiry(candidates, family, fromId) {
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
const
|
|
3656
|
-
const
|
|
3657
|
-
|
|
3658
|
-
|
|
3860
|
+
const now = Date.now();
|
|
3861
|
+
return candidates.map((acc, index) => {
|
|
3862
|
+
const q = acc.quotas?.[family];
|
|
3863
|
+
const weeklyMs = q?.weeklyResetTime ? Date.parse(q.weeklyResetTime) : NaN;
|
|
3864
|
+
const fiveHourMs = q?.resetTime ? Date.parse(q.resetTime) : NaN;
|
|
3865
|
+
const weeklyRemaining = !Number.isNaN(weeklyMs) ? Math.max(0, weeklyMs - now) : Number.POSITIVE_INFINITY;
|
|
3866
|
+
const fiveHourRemaining = !Number.isNaN(fiveHourMs) ? Math.max(0, fiveHourMs - now) : Number.POSITIVE_INFINITY;
|
|
3867
|
+
return {
|
|
3868
|
+
acc,
|
|
3869
|
+
index,
|
|
3870
|
+
weeklyRemaining,
|
|
3871
|
+
fiveHourRemaining,
|
|
3872
|
+
weeklyKnown: Number.isFinite(weeklyRemaining),
|
|
3873
|
+
fiveHourKnown: Number.isFinite(fiveHourRemaining)
|
|
3874
|
+
};
|
|
3875
|
+
}).sort((a, b) => {
|
|
3876
|
+
if (a.weeklyKnown && b.weeklyKnown) {
|
|
3877
|
+
if (a.weeklyRemaining !== b.weeklyRemaining) return a.weeklyRemaining - b.weeklyRemaining;
|
|
3878
|
+
} else if (a.weeklyKnown !== b.weeklyKnown) return a.weeklyKnown ? -1 : 1;
|
|
3879
|
+
if (a.fiveHourKnown && b.fiveHourKnown) {
|
|
3880
|
+
if (a.fiveHourRemaining !== b.fiveHourRemaining) return a.fiveHourRemaining - b.fiveHourRemaining;
|
|
3881
|
+
} else if (a.fiveHourKnown !== b.fiveHourKnown) return a.fiveHourKnown ? -1 : 1;
|
|
3659
3882
|
const da = this.cyclicDistance(fromId, a.acc.id);
|
|
3660
3883
|
const db = this.cyclicDistance(fromId, b.acc.id);
|
|
3661
3884
|
return da !== db ? da - db : a.index - b.index;
|
|
@@ -3781,7 +4004,7 @@ var AccountPoolManager = class {
|
|
|
3781
4004
|
return activeCandidate;
|
|
3782
4005
|
}
|
|
3783
4006
|
}
|
|
3784
|
-
const nextAccount =
|
|
4007
|
+
const nextAccount = this.orderByExpiry(candidates, family, activeId)[0];
|
|
3785
4008
|
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
3786
4009
|
this.data.activeAccountIds[family] = nextAccount.id;
|
|
3787
4010
|
this.data.lastActiveAccountId = nextAccount.id;
|
|
@@ -28739,14 +28962,14 @@ function mergeSentImageIds(existing, added, max = 64) {
|
|
|
28739
28962
|
* budget is filled from the newest other turns.
|
|
28740
28963
|
*/
|
|
28741
28964
|
function buildDigest(messages, fromIdx, maxChars) {
|
|
28742
|
-
const taskText = latestUserTaskText(messages.slice(fromIdx));
|
|
28965
|
+
const taskText = stripNulBytes(latestUserTaskText(messages.slice(fromIdx)));
|
|
28743
28966
|
const taskLine = taskText !== "" ? "User: " + taskText : "";
|
|
28744
28967
|
const otherLines = [];
|
|
28745
28968
|
for (let i = fromIdx; i < messages.length; i++) {
|
|
28746
28969
|
const m = messages[i];
|
|
28747
28970
|
if (m === void 0 || m.role === "system") continue;
|
|
28748
28971
|
if (isSyntheticHarnessMessage(m)) continue;
|
|
28749
|
-
const text = textOf(m);
|
|
28972
|
+
const text = stripNulBytes(textOf(m));
|
|
28750
28973
|
if (text === "") continue;
|
|
28751
28974
|
if (taskLine !== "" && !isToolResultMessage(m) && m.role === "user" && text === taskText) continue;
|
|
28752
28975
|
otherLines.push(digestLabel(m) + text);
|
|
@@ -28809,7 +29032,9 @@ function brief(s) {
|
|
|
28809
29032
|
}
|
|
28810
29033
|
function sawAuthFailure(parser, outcome) {
|
|
28811
29034
|
if (parser.stats.sawAuthFailure) return true;
|
|
28812
|
-
|
|
29035
|
+
if (looksLikeAuthFailure(outcome.stderrTail)) return true;
|
|
29036
|
+
if (parser.stats.lastResultError && looksLikeAuthFailure(parser.stats.lastResultError)) return true;
|
|
29037
|
+
return false;
|
|
28813
29038
|
}
|
|
28814
29039
|
/**
|
|
28815
29040
|
* Bounded wait for an aborted run to settle, so a steer replacement never
|
|
@@ -29068,12 +29293,29 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29068
29293
|
this.warnOnce("cwd:" + (sessionKey || "anon"), "session workspace unresolved (sessionId=" + (sessionKey || "none") + ") — running agy in the DSH process cwd: " + workspaceRoot);
|
|
29069
29294
|
}
|
|
29070
29295
|
}
|
|
29296
|
+
if (isGoalToolContinuation(options.messages) || isPlanToolContinuation(options.messages)) {
|
|
29297
|
+
yield {
|
|
29298
|
+
type: "finish",
|
|
29299
|
+
reason: { kind: "stop" }
|
|
29300
|
+
};
|
|
29301
|
+
return;
|
|
29302
|
+
}
|
|
29303
|
+
const goalContext = detectGoalContext(options.messages);
|
|
29304
|
+
if (goalContext && (!goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
|
|
29305
|
+
const sg = this.deps.sessionGoal(sessionKey);
|
|
29306
|
+
if (sg) {
|
|
29307
|
+
if (!goalContext.goalId) goalContext.goalId = sg.goalId;
|
|
29308
|
+
if (goalContext.revision === void 0) goalContext.revision = sg.revision;
|
|
29309
|
+
}
|
|
29310
|
+
}
|
|
29311
|
+
const supportsGoalUpdate = Boolean(options.tools?.some((t) => t.name === "update_goal"));
|
|
29312
|
+
const supportsPlanExit = Boolean(options.tools?.some((t) => t.name === "exit_plan_mode")) && isPlanMode(options.messages);
|
|
29071
29313
|
const continuation = detectContinuation(options.messages);
|
|
29072
29314
|
if (continuation !== null) {
|
|
29073
29315
|
const rec = this.deps.runs.get(continuation.runId);
|
|
29074
29316
|
const deadFailure = rec !== void 0 && rec.isSettled ? rec.failureInfo : null;
|
|
29075
29317
|
if (rec !== void 0 && !(deadFailure !== null && this.claimFailover(sessionKey, deadFailure.code))) {
|
|
29076
|
-
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode);
|
|
29318
|
+
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
29077
29319
|
return;
|
|
29078
29320
|
}
|
|
29079
29321
|
if (rec !== void 0) {
|
|
@@ -29163,7 +29405,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29163
29405
|
}
|
|
29164
29406
|
if (!account) {
|
|
29165
29407
|
const pool = this.deps.pool;
|
|
29166
|
-
const
|
|
29408
|
+
const allAccounts = pool.getAccounts();
|
|
29409
|
+
const accounts = allAccounts.filter((a) => a.enabled);
|
|
29410
|
+
const disabled = allAccounts.length - accounts.length;
|
|
29167
29411
|
const cooling = accounts.filter((a) => pool.isAccountEligible(a, family) === false && !a.authRequired).length;
|
|
29168
29412
|
const signedOut = accounts.filter((a) => a.authRequired === true).length;
|
|
29169
29413
|
const countdown = pool.getEarliestAvailableCountdown?.(family) ?? pool.getEarliestResetCountdown(family);
|
|
@@ -29172,7 +29416,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29172
29416
|
account = pool.selectAccount(family) ?? void 0;
|
|
29173
29417
|
}
|
|
29174
29418
|
if (!account) {
|
|
29175
|
-
|
|
29419
|
+
let why;
|
|
29420
|
+
if (accounts.length === 0 && allAccounts.length > 0) why = "all accounts are disabled (enable an account in the console or run /agy enable-account)";
|
|
29421
|
+
else if (signedOut > 0 && signedOut === accounts.length) why = "every account needs a re-login (run /agy auth)";
|
|
29422
|
+
else {
|
|
29423
|
+
const details = [];
|
|
29424
|
+
if (cooling > 0) details.push(`${cooling} in cooldown`);
|
|
29425
|
+
if (signedOut > 0) details.push(`${signedOut} signed out`);
|
|
29426
|
+
if (disabled > 0) details.push(`${disabled} disabled`);
|
|
29427
|
+
const detailStr = details.length > 0 ? ` (${details.join(", ")})` : "";
|
|
29428
|
+
why = `no account can serve ${family} right now${detailStr}`;
|
|
29429
|
+
}
|
|
29176
29430
|
const waitStr = countdown !== null && countdown > 0 ? ` (earliest reset in ${Math.ceil(countdown / 1e3)}s)` : "";
|
|
29177
29431
|
throw new LlmError(`All Antigravity accounts are unavailable: ${why}${waitStr}. Add an account, wait for reset, or run /agy clear-cooldown.`, Err.AGY_ERROR);
|
|
29178
29432
|
}
|
|
@@ -29294,7 +29548,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29294
29548
|
startedAt: now,
|
|
29295
29549
|
inFlight: true
|
|
29296
29550
|
});
|
|
29297
|
-
yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode);
|
|
29551
|
+
yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
29298
29552
|
return;
|
|
29299
29553
|
}
|
|
29300
29554
|
throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
|
|
@@ -29379,6 +29633,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29379
29633
|
};
|
|
29380
29634
|
const activeModelForArgs = activeModel === "" ? cfg.defaultModel : activeModel;
|
|
29381
29635
|
const effectiveAddDirs = workspaceRoot !== "" ? stagedDirs.includes(workspaceRoot) ? stagedDirs : [workspaceRoot, ...stagedDirs] : stagedDirs;
|
|
29636
|
+
prompt = stripNulBytes(prompt);
|
|
29382
29637
|
const argsBase = this.buildArgs({
|
|
29383
29638
|
prompt,
|
|
29384
29639
|
model: activeModelForArgs,
|
|
@@ -29544,31 +29799,31 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29544
29799
|
code: Err.TIMEOUT,
|
|
29545
29800
|
message: "agy run was idle for " + cfg.timeoutMs + "ms without output" + (silentTimeout ? " — agy never emitted a single event; it is likely unable to reach Google (check proxy/network). 无法连接 Google,请检查代理或网络配置" : "")
|
|
29546
29801
|
};
|
|
29547
|
-
else if (
|
|
29548
|
-
|
|
29549
|
-
code: Err.NETWORK,
|
|
29550
|
-
message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
|
|
29551
|
-
} : {
|
|
29552
|
-
kind: "error",
|
|
29553
|
-
code: Err.AUTH,
|
|
29554
|
-
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
29555
|
-
};
|
|
29556
|
-
else if (isRateLimit) {
|
|
29557
|
-
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
29558
|
-
failure = {
|
|
29802
|
+
else if (!consumable) {
|
|
29803
|
+
if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
|
|
29559
29804
|
kind: "error",
|
|
29560
|
-
code:
|
|
29561
|
-
message: "Google
|
|
29562
|
-
}
|
|
29563
|
-
} else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
|
|
29564
|
-
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
29565
|
-
failure = {
|
|
29805
|
+
code: Err.NETWORK,
|
|
29806
|
+
message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
|
|
29807
|
+
} : {
|
|
29566
29808
|
kind: "error",
|
|
29567
|
-
code: Err.
|
|
29568
|
-
message:
|
|
29809
|
+
code: Err.AUTH,
|
|
29810
|
+
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
29569
29811
|
};
|
|
29570
|
-
|
|
29571
|
-
|
|
29812
|
+
else if (isRateLimit) {
|
|
29813
|
+
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
29814
|
+
failure = {
|
|
29815
|
+
kind: "error",
|
|
29816
|
+
code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
|
|
29817
|
+
message: "Google Antigravity quota / rate limit reached: " + bestMsg
|
|
29818
|
+
};
|
|
29819
|
+
} else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
|
|
29820
|
+
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
29821
|
+
failure = {
|
|
29822
|
+
kind: "error",
|
|
29823
|
+
code: Err.ELIGIBILITY,
|
|
29824
|
+
message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
|
|
29825
|
+
};
|
|
29826
|
+
} else if (outcome.code !== 0) {
|
|
29572
29827
|
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
29573
29828
|
failure = transportFailure ? {
|
|
29574
29829
|
kind: "error",
|
|
@@ -29648,7 +29903,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29648
29903
|
message: "internal error: " + brief(String(err))
|
|
29649
29904
|
});
|
|
29650
29905
|
});
|
|
29651
|
-
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode);
|
|
29906
|
+
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
29652
29907
|
}
|
|
29653
29908
|
/**
|
|
29654
29909
|
* Stream one span of a recording: map events from `from` until the mapper
|
|
@@ -29657,18 +29912,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29657
29912
|
* as this span's terminal chunk — the turn ends exactly like a native
|
|
29658
29913
|
* provider error.
|
|
29659
29914
|
*/
|
|
29660
|
-
async *driveSpan(rec, from, cutOnTool, useCodeWrapper) {
|
|
29915
|
+
async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
|
|
29661
29916
|
this.deps.runs.remember(rec);
|
|
29662
29917
|
this.deps.runs.pin(rec.runId);
|
|
29663
29918
|
try {
|
|
29664
|
-
yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper);
|
|
29919
|
+
yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
29665
29920
|
} finally {
|
|
29666
29921
|
this.deps.runs.unpin(rec.runId);
|
|
29667
29922
|
this.deps.runs.remember(rec);
|
|
29668
29923
|
}
|
|
29669
29924
|
}
|
|
29670
29925
|
/** Body of {@link driveSpan}: map recorded events until the span finishes. */
|
|
29671
|
-
async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper) {
|
|
29926
|
+
async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
|
|
29672
29927
|
const queue = new ChunkQueue();
|
|
29673
29928
|
(async () => {
|
|
29674
29929
|
const resolved = /* @__PURE__ */ new Map();
|
|
@@ -29682,7 +29937,10 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29682
29937
|
usage: rec,
|
|
29683
29938
|
resolvedFullArgs: resolved,
|
|
29684
29939
|
resolvedThoughts,
|
|
29685
|
-
mediaBaseUrl: this.deps.getMediaBaseUrl?.() ?? void 0
|
|
29940
|
+
mediaBaseUrl: this.deps.getMediaBaseUrl?.() ?? void 0,
|
|
29941
|
+
goalContext,
|
|
29942
|
+
supportsGoalUpdate,
|
|
29943
|
+
supportsPlanExit
|
|
29686
29944
|
});
|
|
29687
29945
|
let i = from;
|
|
29688
29946
|
try {
|
|
@@ -29868,6 +30126,7 @@ async function runAgyOnce(deps, req) {
|
|
|
29868
30126
|
if (req.effort) args.push("--effort", req.effort);
|
|
29869
30127
|
let prompt = req.prompt;
|
|
29870
30128
|
if (req.readPaths && req.readPaths.length > 0) prompt = await inlineFiles(prompt, req.readPaths, cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd());
|
|
30129
|
+
prompt = stripNulBytes(prompt);
|
|
29871
30130
|
let cleanup = null;
|
|
29872
30131
|
if (req.schema !== void 0 && req.schema !== null) {
|
|
29873
30132
|
const sa = await schemaArgs(req.schema);
|
|
@@ -29922,15 +30181,15 @@ async function runAgyOnce(deps, req) {
|
|
|
29922
30181
|
error: "timed out after " + timeoutMs + "ms",
|
|
29923
30182
|
durationMs: outcome.durationMs
|
|
29924
30183
|
};
|
|
29925
|
-
|
|
30184
|
+
const joined = textParts.join("\n").trim();
|
|
30185
|
+
const text = joined !== "" ? joined : resultText;
|
|
30186
|
+
if (text === "" && (looksLikeAuthFailure(outcome.stderrTail) || parser.stats.sawAuthFailure)) return {
|
|
29926
30187
|
ok: false,
|
|
29927
30188
|
text: "",
|
|
29928
30189
|
conversationId,
|
|
29929
30190
|
error: "agy is not signed in — run /agy auth",
|
|
29930
30191
|
durationMs: outcome.durationMs
|
|
29931
30192
|
};
|
|
29932
|
-
const joined = textParts.join("\n").trim();
|
|
29933
|
-
const text = joined !== "" ? joined : resultText;
|
|
29934
30193
|
if (outcome.code !== 0 && text === "") return {
|
|
29935
30194
|
ok: false,
|
|
29936
30195
|
text: "",
|
|
@@ -30417,7 +30676,11 @@ const HELP = [
|
|
|
30417
30676
|
"**/agy** — Antigravity (agy CLI) bridge",
|
|
30418
30677
|
"- `/agy status` — binary, version, auth, mode, catalog, bindings",
|
|
30419
30678
|
"- `/agy pool` / `/agy accounts` — account pool, live quota %, cooldowns",
|
|
30679
|
+
"- `/agy goal [status|complete|clear|pause]` — inspect or control the current DSH goal",
|
|
30680
|
+
"- `/agy session [info|clear]` — view or drop the current session agy binding",
|
|
30420
30681
|
"- `/agy add-account [alias]` — start OAuth login for a new account slot",
|
|
30682
|
+
"- `/agy enable-account <id>` — enable an account slot",
|
|
30683
|
+
"- `/agy disable-account <id>` — disable an account slot (skips in rotation)",
|
|
30421
30684
|
"- `/agy refresh-quota` — query Google backend for fresh quota %",
|
|
30422
30685
|
"- `/agy clear-cooldown` — reset cooldown timers across all accounts",
|
|
30423
30686
|
"- `/agy remove-account <id>` — delete an account slot",
|
|
@@ -30434,12 +30697,12 @@ const HELP = [
|
|
|
30434
30697
|
function agyCommandDefinition(deps) {
|
|
30435
30698
|
return {
|
|
30436
30699
|
name: "agy",
|
|
30437
|
-
description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics",
|
|
30438
|
-
input: { hint: "<status|pool|models|mode|effort|workspace|clear|doctor|help>" },
|
|
30439
|
-
handler: (invocation) => handle(deps, invocation.rawInput)
|
|
30700
|
+
description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics, goal",
|
|
30701
|
+
input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|workspace|clear|doctor|help>" },
|
|
30702
|
+
handler: (invocation) => handle(deps, invocation.rawInput, invocation)
|
|
30440
30703
|
};
|
|
30441
30704
|
}
|
|
30442
|
-
async function handle(deps, raw) {
|
|
30705
|
+
async function handle(deps, raw, invocation) {
|
|
30443
30706
|
const parts = raw.trim().split(/\s+/).filter(Boolean);
|
|
30444
30707
|
const sub = parts[0] ?? "help";
|
|
30445
30708
|
const arg = parts[1] ?? "";
|
|
@@ -30463,10 +30726,21 @@ async function handle(deps, raw) {
|
|
|
30463
30726
|
const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
|
|
30464
30727
|
cds.push(`${fam} cooldown: ${sec}s left (${cd.reason})`);
|
|
30465
30728
|
}
|
|
30466
|
-
|
|
30729
|
+
let statusText = " - Status: 🟢 Ready";
|
|
30730
|
+
if (!a.enabled) statusText = " - Status: ⏸️ Disabled";
|
|
30731
|
+
else if (a.authRequired) statusText = ` - Status: 🔴 Auth required (${a.authError || "sign in again"})`;
|
|
30732
|
+
else if (cds.length > 0) statusText = ` - ⚠️ ${cds.join("; ")}`;
|
|
30733
|
+
lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, statusText);
|
|
30467
30734
|
}
|
|
30468
30735
|
return ok(lines.join("\n"));
|
|
30469
30736
|
}
|
|
30737
|
+
if (sub === "enable-account" || sub === "disable-account") {
|
|
30738
|
+
if (!arg) return err(`usage: /agy ${sub} <id>`);
|
|
30739
|
+
const pool = deps.pool?.();
|
|
30740
|
+
if (!pool) return err("Account pool not available");
|
|
30741
|
+
const enabled = sub === "enable-account";
|
|
30742
|
+
return pool.setAccountEnabled(arg, enabled) ? ok(`${enabled ? "Enabled" : "Disabled"} account ${arg}`) : err(`Account ${arg} not found`);
|
|
30743
|
+
}
|
|
30470
30744
|
if (sub === "add-account") {
|
|
30471
30745
|
const pool = deps.pool?.();
|
|
30472
30746
|
if (!pool) return err("Account pool not available");
|
|
@@ -30498,6 +30772,164 @@ async function handle(deps, raw) {
|
|
|
30498
30772
|
if (!pool) return err("Account pool not available");
|
|
30499
30773
|
return pool.deleteAccount(arg) ? ok(`Removed account ${arg}`) : err(`Account ${arg} not found`);
|
|
30500
30774
|
}
|
|
30775
|
+
if (sub === "goal") {
|
|
30776
|
+
const action = arg || "status";
|
|
30777
|
+
const session = invocation?.agent?.session ?? (deps.sessions?.() && invocation?.agent?.id ? deps.sessions().get(invocation.agent.id) : void 0);
|
|
30778
|
+
if (!session) return err("No active DSH session available for goal command.");
|
|
30779
|
+
const events = session.events;
|
|
30780
|
+
let latestGoalChange = null;
|
|
30781
|
+
if (events) for (let i = events.length - 1; i >= 0; i--) {
|
|
30782
|
+
const ev = events[i];
|
|
30783
|
+
if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
|
|
30784
|
+
const d = ev.data;
|
|
30785
|
+
if (d.kind === "goal/change") {
|
|
30786
|
+
latestGoalChange = d;
|
|
30787
|
+
break;
|
|
30788
|
+
}
|
|
30789
|
+
}
|
|
30790
|
+
}
|
|
30791
|
+
const activeGoal = latestGoalChange && latestGoalChange.operation !== "clear" ? latestGoalChange.goal : null;
|
|
30792
|
+
if (action === "status") {
|
|
30793
|
+
if (!activeGoal || !latestGoalChange) return ok("**DSH Goal Status**: No active goal in this session.");
|
|
30794
|
+
return ok([
|
|
30795
|
+
`**DSH Goal Status** (${{
|
|
30796
|
+
active: "🟢",
|
|
30797
|
+
paused: "⏸️",
|
|
30798
|
+
blocked: "🚫",
|
|
30799
|
+
complete: "✅"
|
|
30800
|
+
}[activeGoal.phase] ?? "⚪"} \`${activeGoal.phase}\`)`,
|
|
30801
|
+
`- **Goal ID**: \`${activeGoal.id}\` (rev ${activeGoal.revision})`,
|
|
30802
|
+
`- **Objective**: ${activeGoal.objective}`,
|
|
30803
|
+
`- **Rounds**: ${latestGoalChange.roundsStarted ?? 1} / ${activeGoal.maxGoalRounds ?? 256}`,
|
|
30804
|
+
...activeGoal.blockedReason ? [`- **Blocked Reason**: ${activeGoal.blockedReason.message}`] : []
|
|
30805
|
+
].join("\n"));
|
|
30806
|
+
}
|
|
30807
|
+
if (action === "complete") {
|
|
30808
|
+
if (!activeGoal || !latestGoalChange) return err("No active goal to complete.");
|
|
30809
|
+
if (typeof session.append === "function") {
|
|
30810
|
+
session.append("goal/change", {
|
|
30811
|
+
kind: "goal/change",
|
|
30812
|
+
version: 1,
|
|
30813
|
+
operation: "complete",
|
|
30814
|
+
goal: {
|
|
30815
|
+
id: activeGoal.id,
|
|
30816
|
+
revision: activeGoal.revision + 1,
|
|
30817
|
+
objective: activeGoal.objective,
|
|
30818
|
+
phase: "complete",
|
|
30819
|
+
maxGoalRounds: activeGoal.maxGoalRounds
|
|
30820
|
+
},
|
|
30821
|
+
roundsStarted: latestGoalChange.roundsStarted ?? 1,
|
|
30822
|
+
createdAt: latestGoalChange.createdAt ?? Date.now(),
|
|
30823
|
+
updatedAt: Date.now()
|
|
30824
|
+
});
|
|
30825
|
+
return ok(`✅ Marked goal \`${activeGoal.id}\` as complete. Automatic continuation rounds stopped.`);
|
|
30826
|
+
}
|
|
30827
|
+
return err("Session cannot append events.");
|
|
30828
|
+
}
|
|
30829
|
+
if (action === "pause") {
|
|
30830
|
+
if (!activeGoal || !latestGoalChange) return err("No active goal to pause.");
|
|
30831
|
+
if (typeof session.append === "function") {
|
|
30832
|
+
session.append("goal/change", {
|
|
30833
|
+
kind: "goal/change",
|
|
30834
|
+
version: 1,
|
|
30835
|
+
operation: "pause",
|
|
30836
|
+
goal: {
|
|
30837
|
+
id: activeGoal.id,
|
|
30838
|
+
revision: activeGoal.revision + 1,
|
|
30839
|
+
objective: activeGoal.objective,
|
|
30840
|
+
phase: "paused",
|
|
30841
|
+
maxGoalRounds: activeGoal.maxGoalRounds
|
|
30842
|
+
},
|
|
30843
|
+
roundsStarted: latestGoalChange.roundsStarted ?? 1,
|
|
30844
|
+
createdAt: latestGoalChange.createdAt ?? Date.now(),
|
|
30845
|
+
updatedAt: Date.now()
|
|
30846
|
+
});
|
|
30847
|
+
return ok(`⏸️ Paused goal \`${activeGoal.id}\`.`);
|
|
30848
|
+
}
|
|
30849
|
+
return err("Session cannot append events.");
|
|
30850
|
+
}
|
|
30851
|
+
if (action === "resume") {
|
|
30852
|
+
if (!activeGoal || !latestGoalChange) return err("No active goal to resume.");
|
|
30853
|
+
if (typeof session.append === "function") {
|
|
30854
|
+
session.append("goal/change", {
|
|
30855
|
+
kind: "goal/change",
|
|
30856
|
+
version: 1,
|
|
30857
|
+
operation: "resume",
|
|
30858
|
+
goal: {
|
|
30859
|
+
id: activeGoal.id,
|
|
30860
|
+
revision: activeGoal.revision + 1,
|
|
30861
|
+
objective: activeGoal.objective,
|
|
30862
|
+
phase: "active",
|
|
30863
|
+
maxGoalRounds: activeGoal.maxGoalRounds
|
|
30864
|
+
},
|
|
30865
|
+
roundsStarted: latestGoalChange.roundsStarted ?? 1,
|
|
30866
|
+
createdAt: latestGoalChange.createdAt ?? Date.now(),
|
|
30867
|
+
updatedAt: Date.now()
|
|
30868
|
+
});
|
|
30869
|
+
return ok(`▶️ Resumed goal \`${activeGoal.id}\`.`);
|
|
30870
|
+
}
|
|
30871
|
+
return err("Session cannot append events.");
|
|
30872
|
+
}
|
|
30873
|
+
if (action === "clear") {
|
|
30874
|
+
if (!activeGoal) return ok("No active goal to clear.");
|
|
30875
|
+
if (typeof session.append === "function") {
|
|
30876
|
+
session.append("goal/change", {
|
|
30877
|
+
kind: "goal/change",
|
|
30878
|
+
version: 1,
|
|
30879
|
+
operation: "clear",
|
|
30880
|
+
cleared: {
|
|
30881
|
+
id: activeGoal.id,
|
|
30882
|
+
revision: activeGoal.revision + 1
|
|
30883
|
+
},
|
|
30884
|
+
clearedAt: Date.now()
|
|
30885
|
+
});
|
|
30886
|
+
return ok(`Cleared goal \`${activeGoal.id}\`.`);
|
|
30887
|
+
}
|
|
30888
|
+
return err("Session cannot append events.");
|
|
30889
|
+
}
|
|
30890
|
+
return err(`Unknown goal action \`${action}\`. Usage: /agy goal [status|complete|pause|resume|clear]`);
|
|
30891
|
+
}
|
|
30892
|
+
if (sub === "session") {
|
|
30893
|
+
const action = arg || "info";
|
|
30894
|
+
const sessionId = invocation?.agent?.id ? String(invocation.agent.id) : void 0;
|
|
30895
|
+
const session = invocation?.agent?.session ?? (deps.sessions?.() && sessionId ? deps.sessions().get(sessionId) : void 0);
|
|
30896
|
+
if (action === "info") {
|
|
30897
|
+
const lines = ["**DSH Session & agy Bridge Info**"];
|
|
30898
|
+
if (sessionId) {
|
|
30899
|
+
lines.push(`- **Session ID**: \`${sessionId}\``);
|
|
30900
|
+
const cwd = session?.header?.cwd;
|
|
30901
|
+
if (cwd) lines.push(`- **Workspace CWD**: \`${cwd}\``);
|
|
30902
|
+
const bindings = deps.store().all();
|
|
30903
|
+
const matchedKey = Object.keys(bindings).find((k) => k === sessionId || k.startsWith(`${sessionId}:`));
|
|
30904
|
+
if (matchedKey && bindings[matchedKey]) {
|
|
30905
|
+
const b = bindings[matchedKey];
|
|
30906
|
+
lines.push(`- **Bound agy Conversation**: \`${b.conversationId}\``, `- **Bound Model**: \`${b.model || "default"}\``, `- **Last Message Count**: ${b.lastMessageCount}`, `- **Last Updated**: ${new Date(b.updatedAt).toLocaleTimeString()}`);
|
|
30907
|
+
} else lines.push("- **Bound agy Conversation**: *(none - next turn starts fresh)*");
|
|
30908
|
+
if (session?.events) lines.push(`- **Total Session Events**: ${session.events.length}`);
|
|
30909
|
+
} else lines.push("- Current session ID could not be identified from invocation context.");
|
|
30910
|
+
return ok(lines.join("\n"));
|
|
30911
|
+
}
|
|
30912
|
+
if (action === "clear") {
|
|
30913
|
+
if (!sessionId) {
|
|
30914
|
+
const all = deps.store().all();
|
|
30915
|
+
const keys = Object.keys(all);
|
|
30916
|
+
if (keys.length === 0) return ok("No conversation bindings yet.");
|
|
30917
|
+
const key = keys.reduce((a, b) => (all[a]?.updatedAt ?? 0) >= (all[b]?.updatedAt ?? 0) ? a : b);
|
|
30918
|
+
const dropped = all[key];
|
|
30919
|
+
if (dropped === void 0) return ok("No conversation bindings yet.");
|
|
30920
|
+
deps.store().delete(key);
|
|
30921
|
+
return ok(`Dropped binding for session \`${key}\` (agy conversation \`${dropped.conversationId}\`). Next turn starts fresh.`);
|
|
30922
|
+
}
|
|
30923
|
+
const bindings = deps.store().all();
|
|
30924
|
+
let count = 0;
|
|
30925
|
+
for (const k of Object.keys(bindings)) if (k === sessionId || k.startsWith(`${sessionId}:`)) {
|
|
30926
|
+
deps.store().delete(k);
|
|
30927
|
+
count++;
|
|
30928
|
+
}
|
|
30929
|
+
return ok(`Dropped ${count > 0 ? count : "any"} agy conversation binding(s) for session \`${sessionId}\`. Next turn will start a fresh agy conversation.`);
|
|
30930
|
+
}
|
|
30931
|
+
return err(`Unknown session action \`${action}\`. Usage: /agy session [info|clear]`);
|
|
30932
|
+
}
|
|
30501
30933
|
if (sub === "auth") {
|
|
30502
30934
|
const flow = deps.poolAuth?.();
|
|
30503
30935
|
if (!flow) return err("auth flow not available");
|
|
@@ -30558,6 +30990,15 @@ async function handle(deps, raw) {
|
|
|
30558
30990
|
return ok("Workspace set to **" + arg + "** — effective next turn.");
|
|
30559
30991
|
}
|
|
30560
30992
|
if (sub === "clear") {
|
|
30993
|
+
const sessionId = invocation?.agent?.id ? String(invocation.agent.id) : void 0;
|
|
30994
|
+
if (sessionId) {
|
|
30995
|
+
const bindings = deps.store().all();
|
|
30996
|
+
const keys = Object.keys(bindings).filter((k) => k === sessionId || k.startsWith(`${sessionId}:`));
|
|
30997
|
+
if (keys.length > 0) {
|
|
30998
|
+
for (const k of keys) deps.store().delete(k);
|
|
30999
|
+
return ok(`Dropped binding for session \`${sessionId}\`. The next turn starts a fresh agy conversation.`);
|
|
31000
|
+
}
|
|
31001
|
+
}
|
|
30561
31002
|
const all = deps.store().all();
|
|
30562
31003
|
const keys = Object.keys(all);
|
|
30563
31004
|
if (keys.length === 0) return ok("No conversation bindings yet.");
|
|
@@ -31941,6 +32382,22 @@ function apply(ctx, entryConfig = {}) {
|
|
|
31941
32382
|
sessionCwd: (sessionId) => {
|
|
31942
32383
|
return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
|
|
31943
32384
|
},
|
|
32385
|
+
sessionGoal: (sessionId) => {
|
|
32386
|
+
const session = (ctx.get("sessions") ?? ctx.sessions)?.get(sessionId);
|
|
32387
|
+
if (!session?.events) return null;
|
|
32388
|
+
for (let i = session.events.length - 1; i >= 0; i--) {
|
|
32389
|
+
const ev = session.events[i];
|
|
32390
|
+
if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
|
|
32391
|
+
const d = ev.data;
|
|
32392
|
+
if (d.operation === "clear") return null;
|
|
32393
|
+
if (d.goal?.id) return {
|
|
32394
|
+
goalId: d.goal.id,
|
|
32395
|
+
revision: typeof d.goal.revision === "number" ? d.goal.revision : 1
|
|
32396
|
+
};
|
|
32397
|
+
}
|
|
32398
|
+
}
|
|
32399
|
+
return null;
|
|
32400
|
+
},
|
|
31944
32401
|
syncSessionModel: (sessionId, model, effort) => {
|
|
31945
32402
|
if (!sessionId) return;
|
|
31946
32403
|
try {
|
|
@@ -32046,6 +32503,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32046
32503
|
quota: () => quota,
|
|
32047
32504
|
lastRun: () => lastRun,
|
|
32048
32505
|
setOverride,
|
|
32506
|
+
sessions: () => ctx.get("sessions") ?? ctx.sessions,
|
|
32049
32507
|
runDoctor: async () => {
|
|
32050
32508
|
return writeDoctorReport({
|
|
32051
32509
|
cfg: getConfig,
|
|
@@ -32415,6 +32873,26 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32415
32873
|
})();
|
|
32416
32874
|
}
|
|
32417
32875
|
});
|
|
32876
|
+
reg({
|
|
32877
|
+
kind: "exact",
|
|
32878
|
+
path: "/plugins/agy-link/pool/enable",
|
|
32879
|
+
handler: (req, res) => {
|
|
32880
|
+
(async () => {
|
|
32881
|
+
if (methodOf(req) !== "POST") {
|
|
32882
|
+
sendJson(res, 405, { error: "POST only" });
|
|
32883
|
+
return;
|
|
32884
|
+
}
|
|
32885
|
+
const body = await readBody(req);
|
|
32886
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
32887
|
+
const enabled = typeof body.enabled === "boolean" ? body.enabled : true;
|
|
32888
|
+
const ok = pool.setAccountEnabled(id, enabled);
|
|
32889
|
+
sendJson(res, ok ? 200 : 400, {
|
|
32890
|
+
ok,
|
|
32891
|
+
pool: pool.getPoolData()
|
|
32892
|
+
});
|
|
32893
|
+
})();
|
|
32894
|
+
}
|
|
32895
|
+
});
|
|
32418
32896
|
reg({
|
|
32419
32897
|
kind: "exact",
|
|
32420
32898
|
path: "/plugins/agy-link/pool/proxy",
|
|
@@ -32515,6 +32993,25 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32515
32993
|
})();
|
|
32516
32994
|
}
|
|
32517
32995
|
});
|
|
32996
|
+
reg({
|
|
32997
|
+
kind: "exact",
|
|
32998
|
+
path: "/plugins/agy-link/pool/set-active",
|
|
32999
|
+
handler: (req, res) => {
|
|
33000
|
+
(async () => {
|
|
33001
|
+
if (methodOf(req) !== "POST") {
|
|
33002
|
+
sendJson(res, 405, { error: "POST only" });
|
|
33003
|
+
return;
|
|
33004
|
+
}
|
|
33005
|
+
const body = await readBody(req);
|
|
33006
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
33007
|
+
const ok = id !== "" && pool.setActiveAccount(id);
|
|
33008
|
+
sendJson(res, ok ? 200 : 400, {
|
|
33009
|
+
ok,
|
|
33010
|
+
pool: pool.getPoolData()
|
|
33011
|
+
});
|
|
33012
|
+
})();
|
|
33013
|
+
}
|
|
33014
|
+
});
|
|
32518
33015
|
reg({
|
|
32519
33016
|
kind: "exact",
|
|
32520
33017
|
path: "/plugins/agy-link/pool/reorder",
|