@songsid/agend 2.1.4-beta.13 → 2.1.4-beta.15
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/dist/backend/codex.js +29 -3
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +1 -0
- package/dist/backend/kiro.js +14 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/daemon.d.ts +41 -3
- package/dist/daemon.js +127 -16
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +60 -2
- package/dist/fleet-manager.js +178 -6
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
- package/dist/outbound-handlers.d.ts +4 -0
- package/dist/outbound-handlers.js +36 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -36,6 +36,25 @@ export declare function selectLruEvictions(warm: string[], cap: number, opts: {
|
|
|
36
36
|
isIdle: (name: string) => boolean;
|
|
37
37
|
lastInboundAt: (name: string) => number;
|
|
38
38
|
}): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Answer shape for `list_models`. `scope` reports where the LIST came from —
|
|
41
|
+
* "instance" only when it was read through that instance's own backend config,
|
|
42
|
+
* "global" for the account/CLI catalog — so a caller can tell an authoritative
|
|
43
|
+
* per-instance list from a best-effort account-wide one.
|
|
44
|
+
*/
|
|
45
|
+
export interface ModelCatalog {
|
|
46
|
+
backend: string;
|
|
47
|
+
scope: "instance" | "global";
|
|
48
|
+
/** Set whenever an instance was asked about, even if the list is global. */
|
|
49
|
+
instance?: string;
|
|
50
|
+
current_model: string | null;
|
|
51
|
+
models: import("./backend/types.js").ModelOption[];
|
|
52
|
+
/** "cache" = startup probe cache, "live" = probed now, "fallback" = none available. */
|
|
53
|
+
source: "cache" | "live" | "fallback";
|
|
54
|
+
probed_at?: string;
|
|
55
|
+
/** Caveat the caller should read before trusting the list. */
|
|
56
|
+
note?: string;
|
|
57
|
+
}
|
|
39
58
|
export interface DeliveryOptions {
|
|
40
59
|
/** Explicitly identify agent-to-agent delivery when metadata is unavailable. */
|
|
41
60
|
isCrossInstance?: boolean;
|
|
@@ -270,6 +289,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
270
289
|
*/
|
|
271
290
|
private enforceWarmCap;
|
|
272
291
|
private waitForInstanceIdle;
|
|
292
|
+
/**
|
|
293
|
+
* Ask the daemon to capture the pane before answering, then wait for any state
|
|
294
|
+
* report produced after this request. The ordinary query is intentionally
|
|
295
|
+
* cache-only (idle gates call it frequently); this opt-in refresh is reserved
|
|
296
|
+
* for lifecycle decisions where a stale state would strand UI.
|
|
297
|
+
*/
|
|
298
|
+
private refreshInstanceExecutionState;
|
|
273
299
|
private deliverWithIdleGate;
|
|
274
300
|
/**
|
|
275
301
|
* Hand a payload to an instance's IPC, waiting out a *transient* disconnect.
|
|
@@ -693,10 +719,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
693
719
|
private startProgressTicker;
|
|
694
720
|
/**
|
|
695
721
|
* After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
|
|
696
|
-
* it has not, retire its button.
|
|
697
|
-
*
|
|
722
|
+
* it has not, retire its button. The daemon is asked for a fresh pane capture
|
|
723
|
+
* before deciding. Reading only the transition cache stranded the first
|
|
724
|
+
* post-restart bubble when its startup "working" report never got a matching
|
|
725
|
+
* idle edge. Re-arming replaces the previous timer, so a burst of replies ends
|
|
726
|
+
* with exactly one pending check.
|
|
698
727
|
*/
|
|
699
728
|
private armReplyGrace;
|
|
729
|
+
private finishReplyGrace;
|
|
700
730
|
/** Retire (delete) every cancel button belonging to an instance. */
|
|
701
731
|
private retireInstanceButtons;
|
|
702
732
|
/** Begin retiring one button (delete + bounded retry on failure). Idempotent:
|
|
@@ -848,6 +878,34 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
848
878
|
private probeCliEnvs;
|
|
849
879
|
/** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
|
|
850
880
|
private getModelOptions;
|
|
881
|
+
/**
|
|
882
|
+
* Model catalog behind the `list_models` tool.
|
|
883
|
+
*
|
|
884
|
+
* The two scopes are not cosmetic. "global" is the account/CLI catalog served
|
|
885
|
+
* from the startup probe cache; "instance" is resolved through that instance's
|
|
886
|
+
* OWN backend config, and for a Codex instance on a custom provider that is a
|
|
887
|
+
* different catalog entirely — `listModels()` reads models_cache.json out of
|
|
888
|
+
* the instance's private CODEX_HOME. Answering such an instance with the
|
|
889
|
+
* account list would name models its CLI rejects, which is exactly the
|
|
890
|
+
* mistake this tool exists to prevent.
|
|
891
|
+
*
|
|
892
|
+
* `scope` always describes where the returned LIST came from, not what was
|
|
893
|
+
* asked for: an instance query that falls back to the account catalog reports
|
|
894
|
+
* scope "global" and says so in `note`, rather than implying instance-level
|
|
895
|
+
* accuracy it does not have.
|
|
896
|
+
*
|
|
897
|
+
* Never throws — a model listing is an aid, and failing it must not fail a turn.
|
|
898
|
+
*/
|
|
899
|
+
listModelCatalog(opts?: {
|
|
900
|
+
backend?: string;
|
|
901
|
+
instanceName?: string;
|
|
902
|
+
}): Promise<ModelCatalog>;
|
|
903
|
+
/** The custom provider an instance overrides its backend with, if any. */
|
|
904
|
+
private customProviderFor;
|
|
905
|
+
/** Ask a backend for its catalog using ONE instance's real config. Never throws. */
|
|
906
|
+
private instanceScopedModels;
|
|
907
|
+
/** Account-wide catalog: probe cache first, live probe on miss. */
|
|
908
|
+
private globalModelCatalog;
|
|
851
909
|
/** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
|
|
852
910
|
/** Label an effort choice, marking the one currently configured. */
|
|
853
911
|
private effortChoiceLabel;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -107,6 +107,8 @@ const CANCEL_BTN_IDLE_RETIRE_GRACE_MS = 2_000;
|
|
|
107
107
|
* button alone (the idle edge retires it when the run really ends).
|
|
108
108
|
*/
|
|
109
109
|
const REPLY_RETIRE_GRACE_MS = 2 * 60_000;
|
|
110
|
+
/** Bound for the daemon to capture the pane and answer a post-reply state query. */
|
|
111
|
+
const REPLY_STATE_REFRESH_TIMEOUT_MS = 2_000;
|
|
110
112
|
/**
|
|
111
113
|
* The daemon only broadcasts execution state on TRANSITIONS, so a long
|
|
112
114
|
* single-state run sends nothing for hours. The idle backstop therefore pokes a
|
|
@@ -899,6 +901,48 @@ export class FleetManager {
|
|
|
899
901
|
query();
|
|
900
902
|
});
|
|
901
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* Ask the daemon to capture the pane before answering, then wait for any state
|
|
906
|
+
* report produced after this request. The ordinary query is intentionally
|
|
907
|
+
* cache-only (idle gates call it frequently); this opt-in refresh is reserved
|
|
908
|
+
* for lifecycle decisions where a stale state would strand UI.
|
|
909
|
+
*/
|
|
910
|
+
refreshInstanceExecutionState(instanceName, timeoutMs) {
|
|
911
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
912
|
+
if (!ipc?.connected)
|
|
913
|
+
return Promise.resolve(false);
|
|
914
|
+
const previous = this.instanceStateCache.get(instanceName);
|
|
915
|
+
return new Promise(resolve => {
|
|
916
|
+
let settled = false;
|
|
917
|
+
const finish = (refreshed) => {
|
|
918
|
+
if (settled)
|
|
919
|
+
return;
|
|
920
|
+
settled = true;
|
|
921
|
+
clearTimeout(timeout);
|
|
922
|
+
const waiters = this.instanceIdleWaiters.get(instanceName);
|
|
923
|
+
waiters?.delete(check);
|
|
924
|
+
if (waiters?.size === 0)
|
|
925
|
+
this.instanceIdleWaiters.delete(instanceName);
|
|
926
|
+
resolve(refreshed);
|
|
927
|
+
};
|
|
928
|
+
const check = () => {
|
|
929
|
+
if (this.instanceStateCache.get(instanceName) !== previous)
|
|
930
|
+
finish(true);
|
|
931
|
+
};
|
|
932
|
+
const waiters = this.instanceIdleWaiters.get(instanceName) ?? new Set();
|
|
933
|
+
waiters.add(check);
|
|
934
|
+
this.instanceIdleWaiters.set(instanceName, waiters);
|
|
935
|
+
const timeout = setTimeout(() => finish(false), timeoutMs);
|
|
936
|
+
timeout.unref?.();
|
|
937
|
+
const sent = ipc.send({
|
|
938
|
+
type: "query_instance_state",
|
|
939
|
+
requestId: `reply-grace-${Date.now()}`,
|
|
940
|
+
refresh: true,
|
|
941
|
+
});
|
|
942
|
+
if (!sent)
|
|
943
|
+
finish(false);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
902
946
|
async deliverWithIdleGate(instanceName, payload, timeoutMs) {
|
|
903
947
|
let idleObservedAfter = this.lastDeliveryAt.get(instanceName) ?? 0;
|
|
904
948
|
if (this.lifecycle.isPaused(instanceName)) {
|
|
@@ -5424,8 +5468,11 @@ export class FleetManager {
|
|
|
5424
5468
|
}
|
|
5425
5469
|
/**
|
|
5426
5470
|
* After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
|
|
5427
|
-
* it has not, retire its button.
|
|
5428
|
-
*
|
|
5471
|
+
* it has not, retire its button. The daemon is asked for a fresh pane capture
|
|
5472
|
+
* before deciding. Reading only the transition cache stranded the first
|
|
5473
|
+
* post-restart bubble when its startup "working" report never got a matching
|
|
5474
|
+
* idle edge. Re-arming replaces the previous timer, so a burst of replies ends
|
|
5475
|
+
* with exactly one pending check.
|
|
5429
5476
|
*/
|
|
5430
5477
|
armReplyGrace(instanceName) {
|
|
5431
5478
|
for (const entry of this.cancelButtons.values()) {
|
|
@@ -5437,14 +5484,26 @@ export class FleetManager {
|
|
|
5437
5484
|
entry.replyGraceTimer = undefined;
|
|
5438
5485
|
if (!this.cancelButtons.has(entry.messageId))
|
|
5439
5486
|
return;
|
|
5440
|
-
|
|
5441
|
-
return; // resumed — a long run keeps its button
|
|
5442
|
-
this.logger.info({ instanceName, messageId: entry.messageId }, "Cancel button retired — no work resumed after reply");
|
|
5443
|
-
this.retireButton(entry);
|
|
5487
|
+
void this.finishReplyGrace(instanceName, entry);
|
|
5444
5488
|
}, REPLY_RETIRE_GRACE_MS);
|
|
5445
5489
|
entry.replyGraceTimer.unref?.();
|
|
5446
5490
|
}
|
|
5447
5491
|
}
|
|
5492
|
+
async finishReplyGrace(instanceName, entry) {
|
|
5493
|
+
const refreshed = await this.refreshInstanceExecutionState(instanceName, REPLY_STATE_REFRESH_TIMEOUT_MS);
|
|
5494
|
+
if (!this.cancelButtons.has(entry.messageId) || entry.retiring)
|
|
5495
|
+
return;
|
|
5496
|
+
if (!refreshed) {
|
|
5497
|
+
// Fail safe: without an authoritative answer, retain a potentially live
|
|
5498
|
+
// Cancel button. The 5-minute/30-minute/24-hour safety nets still apply.
|
|
5499
|
+
this.logger.debug({ instanceName, messageId: entry.messageId }, "Cancel reply-grace state refresh timed out");
|
|
5500
|
+
return;
|
|
5501
|
+
}
|
|
5502
|
+
if (!this.getInstanceIdle(instanceName))
|
|
5503
|
+
return; // genuine long run keeps its button
|
|
5504
|
+
this.logger.info({ instanceName, messageId: entry.messageId }, "Cancel button retired — no work resumed after reply");
|
|
5505
|
+
this.retireButton(entry);
|
|
5506
|
+
}
|
|
5448
5507
|
/** Retire (delete) every cancel button belonging to an instance. */
|
|
5449
5508
|
retireInstanceButtons(instanceName) {
|
|
5450
5509
|
// Snapshot first — retireButton may delete entries from the map on success.
|
|
@@ -6457,6 +6516,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
6457
6516
|
return null;
|
|
6458
6517
|
const probed = await be.probeCLIEnv({ workingDirectory: "", instanceDir: join(getAgendHome(), "cli-env"), instanceName: `probe-${backend}`, mcpServers: {} });
|
|
6459
6518
|
const env = { backend, probedAt: Date.now(), ...probed };
|
|
6519
|
+
// An empty result must never overwrite a catalog we already have. Some
|
|
6520
|
+
// probes hit the network (`agy models` fetches, 5s cap), so a slow moment
|
|
6521
|
+
// returns [] — and writing that would blank the list for the whole 24h
|
|
6522
|
+
// TTL, long after the CLI recovered. Observed live: a good 11-model
|
|
6523
|
+
// antigravity cache replaced by an empty one. Keep the known models and
|
|
6524
|
+
// let the fresher currentModel/version through.
|
|
6525
|
+
if (!env.models?.length) {
|
|
6526
|
+
const previous = this.readCliEnv(backend);
|
|
6527
|
+
if (previous?.models?.length)
|
|
6528
|
+
env.models = previous.models;
|
|
6529
|
+
}
|
|
6460
6530
|
const path = this.cliEnvPath(backend);
|
|
6461
6531
|
mkdirSync(dirname(path), { recursive: true });
|
|
6462
6532
|
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
@@ -6495,6 +6565,108 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
6495
6565
|
const env = await this.probeBackend(backendName);
|
|
6496
6566
|
return env?.models ?? [];
|
|
6497
6567
|
}
|
|
6568
|
+
/**
|
|
6569
|
+
* Model catalog behind the `list_models` tool.
|
|
6570
|
+
*
|
|
6571
|
+
* The two scopes are not cosmetic. "global" is the account/CLI catalog served
|
|
6572
|
+
* from the startup probe cache; "instance" is resolved through that instance's
|
|
6573
|
+
* OWN backend config, and for a Codex instance on a custom provider that is a
|
|
6574
|
+
* different catalog entirely — `listModels()` reads models_cache.json out of
|
|
6575
|
+
* the instance's private CODEX_HOME. Answering such an instance with the
|
|
6576
|
+
* account list would name models its CLI rejects, which is exactly the
|
|
6577
|
+
* mistake this tool exists to prevent.
|
|
6578
|
+
*
|
|
6579
|
+
* `scope` always describes where the returned LIST came from, not what was
|
|
6580
|
+
* asked for: an instance query that falls back to the account catalog reports
|
|
6581
|
+
* scope "global" and says so in `note`, rather than implying instance-level
|
|
6582
|
+
* accuracy it does not have.
|
|
6583
|
+
*
|
|
6584
|
+
* Never throws — a model listing is an aid, and failing it must not fail a turn.
|
|
6585
|
+
*/
|
|
6586
|
+
async listModelCatalog(opts = {}) {
|
|
6587
|
+
const { instanceName } = opts;
|
|
6588
|
+
if (instanceName) {
|
|
6589
|
+
const backend = this.backendNameForInstance(instanceName);
|
|
6590
|
+
const resolved = this.resolveInstanceModel(instanceName);
|
|
6591
|
+
const currentModel = resolved.source === "unresolved" ? null : resolved.model;
|
|
6592
|
+
const provider = this.customProviderFor(instanceName, backend);
|
|
6593
|
+
const scoped = await this.instanceScopedModels(instanceName, backend);
|
|
6594
|
+
if (scoped.length) {
|
|
6595
|
+
return {
|
|
6596
|
+
backend, scope: "instance", instance: instanceName,
|
|
6597
|
+
current_model: currentModel, models: scoped, source: "live",
|
|
6598
|
+
...(provider ? { note: `Catalog read through this instance's ${backend} provider "${provider}" — it may differ from the account catalog.` } : {}),
|
|
6599
|
+
};
|
|
6600
|
+
}
|
|
6601
|
+
// No instance-local catalog (never launched, or the backend has no
|
|
6602
|
+
// per-instance list). The account catalog is the best available answer,
|
|
6603
|
+
// but it is labelled honestly rather than dressed up as instance scope.
|
|
6604
|
+
const global = await this.globalModelCatalog(backend);
|
|
6605
|
+
return {
|
|
6606
|
+
...global, instance: instanceName, current_model: currentModel,
|
|
6607
|
+
note: provider
|
|
6608
|
+
? `No instance-local catalog yet; showing the account catalog, which may NOT match this instance's ${backend} provider "${provider}".`
|
|
6609
|
+
: "No instance-local catalog yet; showing the account catalog.",
|
|
6610
|
+
};
|
|
6611
|
+
}
|
|
6612
|
+
return this.globalModelCatalog(opts.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code");
|
|
6613
|
+
}
|
|
6614
|
+
/** The custom provider an instance overrides its backend with, if any. */
|
|
6615
|
+
customProviderFor(instanceName, backend) {
|
|
6616
|
+
const opts = this.fleetConfig?.instances?.[instanceName]?.backend_options?.[backend]
|
|
6617
|
+
?? this.fleetConfig?.defaults?.backend_options?.[backend];
|
|
6618
|
+
const provider = opts?.provider;
|
|
6619
|
+
return typeof provider === "string" && provider.trim() ? provider.trim() : null;
|
|
6620
|
+
}
|
|
6621
|
+
/** Ask a backend for its catalog using ONE instance's real config. Never throws. */
|
|
6622
|
+
async instanceScopedModels(instanceName, backend) {
|
|
6623
|
+
try {
|
|
6624
|
+
const inst = this.fleetConfig?.instances?.[instanceName];
|
|
6625
|
+
const instanceDir = this.getInstanceDir(instanceName);
|
|
6626
|
+
const be = createBackend(backend, instanceDir);
|
|
6627
|
+
if (!be.listModels)
|
|
6628
|
+
return [];
|
|
6629
|
+
return await be.listModels({
|
|
6630
|
+
workingDirectory: inst?.working_directory ?? "",
|
|
6631
|
+
instanceDir,
|
|
6632
|
+
instanceName,
|
|
6633
|
+
mcpServers: {},
|
|
6634
|
+
model: inst?.model,
|
|
6635
|
+
backendOptions: inst?.backend_options?.[backend] ?? this.fleetConfig?.defaults?.backend_options?.[backend],
|
|
6636
|
+
}) ?? [];
|
|
6637
|
+
}
|
|
6638
|
+
catch {
|
|
6639
|
+
// listModels is documented never to throw, but a backend constructor can
|
|
6640
|
+
// (missing binary). A catalog is an aid; degrade to the account list.
|
|
6641
|
+
return [];
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
/** Account-wide catalog: probe cache first, live probe on miss. */
|
|
6645
|
+
async globalModelCatalog(backend) {
|
|
6646
|
+
const cached = this.readCliEnv(backend);
|
|
6647
|
+
if (cached?.models?.length) {
|
|
6648
|
+
return {
|
|
6649
|
+
backend, scope: "global", current_model: cached.currentModel ?? null,
|
|
6650
|
+
models: cached.models, source: "cache",
|
|
6651
|
+
probed_at: new Date(cached.probedAt).toISOString(),
|
|
6652
|
+
};
|
|
6653
|
+
}
|
|
6654
|
+
const env = await this.probeBackend(backend);
|
|
6655
|
+
if (env?.models?.length) {
|
|
6656
|
+
return {
|
|
6657
|
+
backend, scope: "global", current_model: env.currentModel ?? null,
|
|
6658
|
+
models: env.models, source: "live",
|
|
6659
|
+
probed_at: new Date(env.probedAt).toISOString(),
|
|
6660
|
+
};
|
|
6661
|
+
}
|
|
6662
|
+
// Reported rather than thrown: "we could not enumerate" is a useful answer,
|
|
6663
|
+
// and the caller can still set a model by name (AgEnD passes it through).
|
|
6664
|
+
return {
|
|
6665
|
+
backend, scope: "global", current_model: env?.currentModel ?? null,
|
|
6666
|
+
models: [], source: "fallback",
|
|
6667
|
+
note: `Could not enumerate models for ${backend} (CLI missing, not logged in, or it offers no list). Model names are passed through to the CLI, so a known-good name still works.`,
|
|
6668
|
+
};
|
|
6669
|
+
}
|
|
6498
6670
|
/** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
|
|
6499
6671
|
/** Label an effort choice, marking the one currently configured. */
|
|
6500
6672
|
effortChoiceLabel(level, current) {
|