super-dsh 0.1.0 → 0.1.2
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-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- package/client.js +0 -4
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-hermes provider — Hermes provider plugin for DeepSeek Harness
|
|
3
|
+
* (Agent Worlds AW-H, TUI gateway line).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the built-in agent loop with an `AgentFactory` that drives a
|
|
6
|
+
* `HermesGatewayClient` (one spawned `python -m tui_gateway.entry` child per
|
|
7
|
+
* session) and bridges the projected wire stream into the Dash Agent/Session
|
|
8
|
+
* contracts. Mirrors `@deepseek-ai/dsh-agent-loop`'s creation transaction
|
|
9
|
+
* (prepare → owned storage handle → setup → publish) so the session + agent
|
|
10
|
+
* publish as one ordered lifecycle AND the DSH session log is written through
|
|
11
|
+
* the upstream `sessionPersistence` service.
|
|
12
|
+
*
|
|
13
|
+
* Session identity (2026-09-17, "mapping only"): the DSH session id is the
|
|
14
|
+
* authority — supplied by the harness at create (`options.sessionId`) and
|
|
15
|
+
* resume (`options.resumeSessionId`). The gateway's session id only
|
|
16
|
+
* materializes after the first prompt, so the pairing is persisted in the
|
|
17
|
+
* adapter's DSH-side app dir (`<home>/dsh-sessions.json`, see hermes-store.ts).
|
|
18
|
+
* Resume resolves that record and fails closed when the gateway session is
|
|
19
|
+
* unknown. Lazy materialization: `createAgent` spawns the gateway child but
|
|
20
|
+
* binds NO gateway session — the FIRST prompt does `ensureStarted()` +
|
|
21
|
+
* `createSession` (fresh) / `resumeSession` (re-attach), and
|
|
22
|
+
* `followGatewaySessionId` upserts the map once the session id materializes.
|
|
23
|
+
*/
|
|
24
|
+
import { realpathSync, statSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
27
|
+
import { emitAgentEvent, installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
28
|
+
import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
|
|
29
|
+
import { HermesGatewayClient, HERMES_SPAWN_GRACE_MS } from "./hermes-client.js";
|
|
30
|
+
import { HermesAgent } from "./agent.js";
|
|
31
|
+
import { HermesLlmAdapter, HERMES_PROVIDER_ID } from "./adapter.js";
|
|
32
|
+
import { SingleHermesPresetRoster } from "./agent-preset-hermes.js";
|
|
33
|
+
import { hermesAgentPresetProjection } from "./agent-preset-projection.js";
|
|
34
|
+
import { sessionRecord, upsertSession } from "./hermes-store.js";
|
|
35
|
+
import { hermesProviderSlugs, readHermesModelCatalog } from "./models.js";
|
|
36
|
+
import { envApprovalMode, defaultPermissionPreset, presetFromEvents } from "./permission.js";
|
|
37
|
+
/** Realpath of a recorded cwd, accepted only when it names an existing directory. */
|
|
38
|
+
function validatedCwd(cwd) {
|
|
39
|
+
if (cwd === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
const canonical = realpathSync(cwd);
|
|
43
|
+
return statSync(canonical).isDirectory() ? canonical : undefined;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Diagnostic trace (HERMES_TRACE=1 on the dsh process enables stderr tracing). */
|
|
50
|
+
const trace = (...parts) => {
|
|
51
|
+
if (process.env.HERMES_TRACE === "1")
|
|
52
|
+
process.stderr.write(`[hermes-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
|
|
53
|
+
};
|
|
54
|
+
/** Open a read handle, pull the complete stored log, close. */
|
|
55
|
+
async function readStoredEvents(persistence, id, signal) {
|
|
56
|
+
const handle = await persistence.open(id, "read", signal === undefined ? {} : { signal });
|
|
57
|
+
try {
|
|
58
|
+
return (await handle.read(0, undefined, signal === undefined ? {} : { signal })).events;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await handle.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class HermesProvider extends Service {
|
|
65
|
+
/**
|
|
66
|
+
* `agentDefaultModel` and `settings` are load-bearing, not decorative (OMP
|
|
67
|
+
* twin, same boot-order trap): the harness owns the "default for new sessions"
|
|
68
|
+
* selection there, and this bridge is the only thing that feeds it the hermes
|
|
69
|
+
* config's real default. Without the inject the context cannot resolve the
|
|
70
|
+
* service at apply time and the push in #registerDefaultModel never fires.
|
|
71
|
+
*/
|
|
72
|
+
static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
|
|
73
|
+
/** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
|
|
74
|
+
runtime;
|
|
75
|
+
/**
|
|
76
|
+
* The world's DSH home — captured once from the world's own `dshHomePath`
|
|
77
|
+
* (never ambient env inside plugins). It anchors adapter-owned DSH state
|
|
78
|
+
* (the session map) only; the hermes runtime keeps its native `~/.hermes`
|
|
79
|
+
* home (2026-09-17 ruling).
|
|
80
|
+
*/
|
|
81
|
+
home;
|
|
82
|
+
/** Echo/staleness guard for the app-wide default-model push. */
|
|
83
|
+
defaultModelKey = "";
|
|
84
|
+
/** Memoized async catalog (probe-client-then-close, resolved once per process). */
|
|
85
|
+
catalogPromise;
|
|
86
|
+
constructor(ctx) {
|
|
87
|
+
super(ctx, "hermesProvider");
|
|
88
|
+
this.runtime = { ctx };
|
|
89
|
+
// Native-home ruling (2026-09-17): the Hermes runtime keeps its own native
|
|
90
|
+
// home (~/.hermes), never redirected. this.home is only the world's DSH
|
|
91
|
+
// home, anchoring adapter-owned DSH state (the session map).
|
|
92
|
+
this.home = this.#resolveHome();
|
|
93
|
+
trace(`home: worldHome=${this.home}`);
|
|
94
|
+
ctx.effect(() => ctx.agents.setFactory(this), "hermesProvider.setFactory()");
|
|
95
|
+
this.#registerModelCatalog();
|
|
96
|
+
this.#registerDefaultModel();
|
|
97
|
+
// The roster must be visible to the API gateway's root-level remote
|
|
98
|
+
// enumeration; register it on this plugin's own (top-level) fiber.
|
|
99
|
+
new SingleHermesPresetRoster(ctx);
|
|
100
|
+
// Drive the `agentPreset` session projection ourselves.
|
|
101
|
+
ctx.inject(["sessionProjections"], (scoped) => {
|
|
102
|
+
scoped.sessionProjections.register(hermesAgentPresetProjection);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The factory owns its sessions' route (user directive 2026-09-15,
|
|
107
|
+
* restated 2026-09-17): the ambient `agentDefaultModel` selection is NOT
|
|
108
|
+
* authoritative here — a session created by this factory always runs on the
|
|
109
|
+
* gateway's real default route {provider: slug, model: id} unless the caller
|
|
110
|
+
* asked for a model this factory serves (a gateway provider slug, or the
|
|
111
|
+
* legacy `hermes` placeholder). The pair is canonicalized through the
|
|
112
|
+
* catalog entry (the r4 dedupe gives each id ONE provider route, so the
|
|
113
|
+
* pinned route always resolves), and ids stay the VERBATIM gateway
|
|
114
|
+
* selection strings. Unavailable catalog → no route fields (the placeholder
|
|
115
|
+
* never leaks — the hermes config default applies).
|
|
116
|
+
*/
|
|
117
|
+
async sessionAgentOptions(requested) {
|
|
118
|
+
const catalog = await this.catalog();
|
|
119
|
+
const requestedProvider = requested?.provider;
|
|
120
|
+
const requestedModel = typeof requested?.model === "string" ? requested.model : undefined;
|
|
121
|
+
const providerServed = requestedProvider === HERMES_PROVIDER_ID ||
|
|
122
|
+
(requestedProvider !== undefined && catalog.models.some((entry) => entry.provider === requestedProvider));
|
|
123
|
+
const explicit = providerServed && requestedModel !== undefined && requestedModel !== "" ? requestedModel : undefined;
|
|
124
|
+
const model = explicit ?? catalog.defaultModel;
|
|
125
|
+
if (model === undefined)
|
|
126
|
+
return { ...(requested ?? {}) };
|
|
127
|
+
// Canonical route: the model's catalog entry pins the real provider slug.
|
|
128
|
+
const entry = catalog.models.find((candidate) => candidate.id === model);
|
|
129
|
+
const provider = entry?.provider ??
|
|
130
|
+
(explicit && requestedProvider !== undefined && requestedProvider !== HERMES_PROVIDER_ID
|
|
131
|
+
? requestedProvider // advisory catalog: pass the served slug through unlisted
|
|
132
|
+
: catalog.provider);
|
|
133
|
+
return { ...(requested ?? {}), provider, model };
|
|
134
|
+
}
|
|
135
|
+
/** The memoized async catalog (probe-client-then-close, resolved once). */
|
|
136
|
+
catalog() {
|
|
137
|
+
if (this.catalogPromise === undefined) {
|
|
138
|
+
this.catalogPromise = this.#probeCatalog();
|
|
139
|
+
}
|
|
140
|
+
return this.catalogPromise;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Probe the gateway `model.options` once and close the probe client. The
|
|
144
|
+
* probe client is the ONLY spawn that arms the idle-probe reaper (explicit
|
|
145
|
+
* spawnGraceMs): it never materializes a session on purpose, so the grace
|
|
146
|
+
* bounds it. Agent-held clients (createAgent/resume) never pass it.
|
|
147
|
+
*/
|
|
148
|
+
async #probeCatalog() {
|
|
149
|
+
const client = HermesGatewayClient.spawn({ spawnGraceMs: HERMES_SPAWN_GRACE_MS });
|
|
150
|
+
try {
|
|
151
|
+
return await readHermesModelCatalog(client);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
client.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The write path is mandatory now (2026-09-16): this factory owns the DSH
|
|
159
|
+
* session log through the upstream persistence service.
|
|
160
|
+
*/
|
|
161
|
+
requirePersistence(op) {
|
|
162
|
+
const persistence = this.runtime.ctx.get("sessionPersistence");
|
|
163
|
+
if (persistence === undefined) {
|
|
164
|
+
throw new Error(`cannot ${op} a Hermes session: session persistence is not configured`);
|
|
165
|
+
}
|
|
166
|
+
return persistence;
|
|
167
|
+
}
|
|
168
|
+
/** Take a fresh session's write ownership (nothing is appended here). */
|
|
169
|
+
async createStoredSession(persistence, session, signal) {
|
|
170
|
+
const handle = await persistence.create(session.header, {
|
|
171
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
172
|
+
...(signal === undefined ? {} : { signal }),
|
|
173
|
+
});
|
|
174
|
+
return { handle, storedCount: 0 };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Runtime facts the agent stamps into the log: Hermes's own system prompt
|
|
178
|
+
* (the settled `session.info` payload's `system_prompt`, captured by the
|
|
179
|
+
* client once non-empty) and the effective route metadata (selected hermes
|
|
180
|
+
* model, else the catalog default, plus the catalog's context window).
|
|
181
|
+
*/
|
|
182
|
+
agentRuntime(client, cwd, resumeStoredSessionId = null) {
|
|
183
|
+
return {
|
|
184
|
+
systemPrompt: () => {
|
|
185
|
+
const prompt = client.systemPrompt;
|
|
186
|
+
return prompt === "" ? undefined : prompt;
|
|
187
|
+
},
|
|
188
|
+
sessionTitle: () => {
|
|
189
|
+
const title = client.title;
|
|
190
|
+
return title === "" ? undefined : title;
|
|
191
|
+
},
|
|
192
|
+
routeContext: async (selection) => {
|
|
193
|
+
const catalog = await this.catalog();
|
|
194
|
+
const model = selection?.model !== undefined && selection.model !== "" ? selection.model : catalog.defaultModel;
|
|
195
|
+
if (model === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
// Canonical route: the entry pins the real provider slug (r4 dedupe
|
|
198
|
+
// gives each id exactly one). A provider-matched find honors an
|
|
199
|
+
// explicit selection; the id-only fallback covers legacy/stale pairs.
|
|
200
|
+
const entry = catalog.models.find((candidate) => candidate.id === model && (selection?.provider === undefined || candidate.provider === selection.provider)) ?? catalog.models.find((candidate) => candidate.id === model);
|
|
201
|
+
return {
|
|
202
|
+
provider: entry?.provider ?? selection?.provider ?? catalog.provider,
|
|
203
|
+
model,
|
|
204
|
+
...(entry?.contextWindow !== undefined && entry.contextWindow > 0 ? { contextWindow: entry.contextWindow } : {}),
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
session: { cwd, resumeStoredSessionId },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** The world's DSH home: boot-provided dshHomePath first, env last. */
|
|
211
|
+
#resolveHome() {
|
|
212
|
+
const fromBoot = this.runtime.ctx.get("dshHomePath");
|
|
213
|
+
if (typeof fromBoot === "string" && fromBoot !== "")
|
|
214
|
+
return fromBoot;
|
|
215
|
+
if (typeof fromBoot === "function") {
|
|
216
|
+
const resolved = fromBoot();
|
|
217
|
+
if (typeof resolved === "string" && resolved !== "")
|
|
218
|
+
return resolved;
|
|
219
|
+
}
|
|
220
|
+
return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "aw");
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Push the gateway's default model into the world's `agentDefaultModel`
|
|
224
|
+
* (one-directional on purpose). The catalog is async, so the push resolves
|
|
225
|
+
* through the memoized promise; `defaultModel === undefined` (unavailable
|
|
226
|
+
* catalog) skips — the placeholder never leaks outward.
|
|
227
|
+
*/
|
|
228
|
+
#registerDefaultModel() {
|
|
229
|
+
this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
|
|
230
|
+
const service = mCtx.get("agentDefaultModel");
|
|
231
|
+
if (service === undefined)
|
|
232
|
+
return;
|
|
233
|
+
void this.catalog()
|
|
234
|
+
.then((catalog) => {
|
|
235
|
+
if (catalog.defaultModel === undefined) {
|
|
236
|
+
trace("default model push skipped: catalog default unavailable");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Real-slug route (2026-09-17 parity ruling): the default model's
|
|
240
|
+
// canonical catalog entry pins the gateway provider slug; the
|
|
241
|
+
// top-level `provider` string is the fallback when the id is
|
|
242
|
+
// somehow unlisted. Ordering note: #registerModelCatalog ran first
|
|
243
|
+
// at boot, so its handle.replace (placeholder → real slugs) on the SAME memoized
|
|
244
|
+
// promise has already swapped the routes in before this push fires.
|
|
245
|
+
const entry = catalog.models.find((candidate) => candidate.id === catalog.defaultModel);
|
|
246
|
+
const target = { provider: entry?.provider ?? catalog.provider, model: catalog.defaultModel };
|
|
247
|
+
const targetKey = `${target.provider}/${target.model}`;
|
|
248
|
+
const current = service.currentSelection?.();
|
|
249
|
+
const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
|
|
250
|
+
if (currentKey === targetKey || currentKey === this.defaultModelKey)
|
|
251
|
+
return;
|
|
252
|
+
trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
|
|
253
|
+
const settings = mCtx.get("settings");
|
|
254
|
+
void Promise.resolve(settings?.replace?.("agent-default-model", target))
|
|
255
|
+
.then(() => service.saveSelection?.(target))
|
|
256
|
+
.then(() => {
|
|
257
|
+
this.defaultModelKey = targetKey;
|
|
258
|
+
this.runtime.ctx.logger.info(`hermes-provider: default model ← hermes catalog: ${targetKey}`);
|
|
259
|
+
})
|
|
260
|
+
.catch((error) => {
|
|
261
|
+
this.runtime.ctx.logger.warn(`hermes-provider: default model save failed: ${String(error)}`);
|
|
262
|
+
});
|
|
263
|
+
})
|
|
264
|
+
.catch((error) => {
|
|
265
|
+
this.runtime.ctx.logger.warn(`hermes-provider: catalog probe failed: ${String(error)}`);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Register the Hermes-backed LLM adapter: the single placeholder route
|
|
271
|
+
* `hermes` at boot (the selector is alive while the async probe runs), then
|
|
272
|
+
* an ATOMIC swap to the distinct real provider slugs once the first catalog
|
|
273
|
+
* probe resolves (`AdapterRegistrationHandle.replace` — validated in full,
|
|
274
|
+
* one synchronous section, no observable gap). 2026-09-17 parity ruling.
|
|
275
|
+
*/
|
|
276
|
+
#registerModelCatalog() {
|
|
277
|
+
const llm = this.runtime.ctx.get("llm");
|
|
278
|
+
if (llm === undefined)
|
|
279
|
+
return;
|
|
280
|
+
// The adapter resolves the async catalog through the memoized supplier.
|
|
281
|
+
const handle = llm.registerAdapter([HERMES_PROVIDER_ID], new HermesLlmAdapter(() => this.catalog()));
|
|
282
|
+
// Registered BEFORE #registerDefaultModel in the constructor, so this
|
|
283
|
+
// .then runs before the default-model push on the same memoized promise:
|
|
284
|
+
// the pushed {provider, model} route always resolves by the time it lands.
|
|
285
|
+
void this.catalog()
|
|
286
|
+
.then((catalog) => {
|
|
287
|
+
const slugs = hermesProviderSlugs(catalog);
|
|
288
|
+
if (slugs.length === 0) {
|
|
289
|
+
this.runtime.ctx.logger.warn("hermes-provider: catalog probe empty — the placeholder hermes route stays");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
handle.replace(slugs);
|
|
293
|
+
this.runtime.ctx.logger.info(`hermes-provider: model routes ← ${slugs.length} gateway provider groups (${slugs.join(", ")})`);
|
|
294
|
+
})
|
|
295
|
+
.catch((error) => {
|
|
296
|
+
this.runtime.ctx.logger.warn(`hermes-provider: model route swap failed: ${String(error)}`);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async createAgent(ownerCtx, options) {
|
|
300
|
+
const loopCtx = this.runtime.ctx;
|
|
301
|
+
const id = options.sessionId;
|
|
302
|
+
const meta = options.meta ?? {};
|
|
303
|
+
const cwd = meta.cwd;
|
|
304
|
+
// Fork: Hermes has no native fork (session.branch is a P2 gap).
|
|
305
|
+
if (meta.parentSession !== undefined) {
|
|
306
|
+
throw new Error(`cannot fork session "${meta.parentSession}" onto the Hermes provider: Hermes has no native session fork`);
|
|
307
|
+
}
|
|
308
|
+
const persistence = this.requirePersistence("create");
|
|
309
|
+
// The preset is record-only (no launch flag — approval is runtime).
|
|
310
|
+
const preset = envApprovalMode() ?? defaultPermissionPreset(loopCtx);
|
|
311
|
+
const spawnCwd = validatedCwd(cwd) ?? process.cwd();
|
|
312
|
+
trace(`create id=${id} preset=${preset ?? "none"} spawnCwd=${spawnCwd}${envApprovalMode() === undefined ? "" : " (env override)"}`);
|
|
313
|
+
// Lazy session materialization (ruling 1): spawn the gateway child (no
|
|
314
|
+
// session bound), record the pairing with gatewaySessionId:null. The FIRST
|
|
315
|
+
// prompt materializes the gateway session via createSession.
|
|
316
|
+
// Agent-held client: the spawn-grace reaper is NEVER armed here (2026-09-17
|
|
317
|
+
// ruling 4 — the session may legitimately materialize far beyond any
|
|
318
|
+
// grace; disposal is the agent lifecycle's job, not a timer's).
|
|
319
|
+
const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
|
|
320
|
+
const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
321
|
+
...(options.seed === undefined ? {} : { seed: options.seed }),
|
|
322
|
+
...(meta === undefined ? {} : { meta }),
|
|
323
|
+
...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
|
|
324
|
+
}));
|
|
325
|
+
let stored;
|
|
326
|
+
let handedOff = false;
|
|
327
|
+
try {
|
|
328
|
+
stored = await this.createStoredSession(persistence, preparation.session, options.signal);
|
|
329
|
+
upsertSession(this.home, String(id), {
|
|
330
|
+
gatewaySessionId: null,
|
|
331
|
+
cwd: spawnCwd,
|
|
332
|
+
createdAt: Date.now(),
|
|
333
|
+
preset: preset ?? null,
|
|
334
|
+
});
|
|
335
|
+
this.followGatewaySessionId(id, client);
|
|
336
|
+
handedOff = true;
|
|
337
|
+
const agentOptions = await this.sessionAgentOptions(options.agentOptions);
|
|
338
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd));
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
trace(`create ${id} THREW: ${String(error)}`);
|
|
342
|
+
client.close();
|
|
343
|
+
if (!handedOff)
|
|
344
|
+
await stored?.handle.close().catch(() => { });
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
if (!handedOff)
|
|
349
|
+
preparation[Symbol.dispose]();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async resume(ownerCtx, options) {
|
|
353
|
+
const loopCtx = this.runtime.ctx;
|
|
354
|
+
const id = options.resumeSessionId;
|
|
355
|
+
// Identity (2026-09-17 "mapping only"): the DSH session id resolves through
|
|
356
|
+
// the adapter-owned map. A missing record (or a never-materialized gateway
|
|
357
|
+
// session) is a genuine unknown: fail closed with the DSH id in the message.
|
|
358
|
+
const record = sessionRecord(this.home, String(id));
|
|
359
|
+
trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.gatewayStoredSessionId ?? "NO-STORED-ID"}`);
|
|
360
|
+
if (record === undefined) {
|
|
361
|
+
throw new Error(`cannot resume session "${id}": no Hermes gateway session is recorded for this Dash session id`);
|
|
362
|
+
}
|
|
363
|
+
if (record.gatewayStoredSessionId === null || record.gatewayStoredSessionId === undefined) {
|
|
364
|
+
throw new Error(`cannot resume session "${id}": no durable gateway session id is recorded (this record predates durable-id capture; re-create the session)`);
|
|
365
|
+
}
|
|
366
|
+
// Spawn cwd comes from the recorded pairing — re-realpath and re-verify.
|
|
367
|
+
const spawnCwd = validatedCwd(record.cwd);
|
|
368
|
+
if (spawnCwd === undefined) {
|
|
369
|
+
throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
|
|
370
|
+
}
|
|
371
|
+
const persistence = this.requirePersistence("resume");
|
|
372
|
+
// Preset (record-only): env override, else the recorded preset, else the
|
|
373
|
+
// persisted session log, else the permission service's default.
|
|
374
|
+
const envMode = envApprovalMode();
|
|
375
|
+
const preset = envMode !== undefined
|
|
376
|
+
? envMode
|
|
377
|
+
: record.preset ??
|
|
378
|
+
presetFromEvents(await readStoredEvents(persistence, id, options.signal)) ??
|
|
379
|
+
defaultPermissionPreset(loopCtx);
|
|
380
|
+
trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"}`);
|
|
381
|
+
// Spawn the gateway child (no session bound); the FIRST prompt re-attaches
|
|
382
|
+
// via resumeSession(record.gatewayStoredSessionId) — the DURABLE key.
|
|
383
|
+
const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
|
|
384
|
+
let handle;
|
|
385
|
+
let stored;
|
|
386
|
+
let preparation;
|
|
387
|
+
let handedOff = false;
|
|
388
|
+
try {
|
|
389
|
+
handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
|
|
390
|
+
const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
|
|
391
|
+
const closers = interruptedTurnClosers(cold.events);
|
|
392
|
+
if (closers.length > 0)
|
|
393
|
+
await handle.append(closers);
|
|
394
|
+
preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
395
|
+
seed: [...cold.events, ...closers],
|
|
396
|
+
meta: structuredClone(handle.header),
|
|
397
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
398
|
+
eventState: cold.eventState,
|
|
399
|
+
}));
|
|
400
|
+
stored = { handle, storedCount: cold.events.length + closers.length };
|
|
401
|
+
trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
|
|
402
|
+
handle = undefined; // ownership passes to setupAndPublish
|
|
403
|
+
this.followGatewaySessionId(id, client);
|
|
404
|
+
handedOff = true;
|
|
405
|
+
const agentOptions = await this.sessionAgentOptions(options.agentOptions);
|
|
406
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd, record.gatewayStoredSessionId));
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
|
|
410
|
+
client.close();
|
|
411
|
+
if (!handedOff)
|
|
412
|
+
await handle?.close().catch(() => { });
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
if (!handedOff)
|
|
417
|
+
preparation?.[Symbol.dispose]();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Persist the Dash↔gateway pairing at session ADOPTION (the
|
|
422
|
+
* session.create/resume response — the materialization moment) and tolerate
|
|
423
|
+
* `stored_session_id` DRIFT: the gateway may remint the durable key on
|
|
424
|
+
* resume, so the upsert always tracks the latest adopted pair (2026-09-17
|
|
425
|
+
* ruling). Arrow FIELD (not a `#` method): the provider is exposed through
|
|
426
|
+
* a Cordis tracing proxy.
|
|
427
|
+
*/
|
|
428
|
+
followGatewaySessionId = (id, client) => {
|
|
429
|
+
const observe = () => {
|
|
430
|
+
const live = client.sessionId;
|
|
431
|
+
const stored = client.storedSessionId;
|
|
432
|
+
if (live === null && stored === null)
|
|
433
|
+
return;
|
|
434
|
+
try {
|
|
435
|
+
upsertSession(this.home, String(id), {
|
|
436
|
+
...(live === null ? {} : { gatewaySessionId: live }),
|
|
437
|
+
...(stored === null ? {} : { gatewayStoredSessionId: stored }),
|
|
438
|
+
});
|
|
439
|
+
trace(`gateway ids recorded for ${String(id)}: live=${live ?? "none"} stored=${stored ?? "none"}`);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
this.runtime.ctx.logger.warn(`hermes-provider: failed to persist the gateway ids for ${String(id)}: ${String(error)}`);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
observe();
|
|
446
|
+
const off = client.onAdopted(() => {
|
|
447
|
+
observe();
|
|
448
|
+
off();
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/** Durable flush of the session's pre-publication suffix. */
|
|
453
|
+
async function appendUnstoredSuffix(stored, session) {
|
|
454
|
+
const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
|
|
455
|
+
if (suffix.length > 0)
|
|
456
|
+
await stored.handle.append(suffix);
|
|
457
|
+
stored.storedCount += suffix.length;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Shared creation/resume transaction: build the agent over the prepared
|
|
461
|
+
* session, run unpublished setup, flush the unstored suffix, and publish both
|
|
462
|
+
* in order. Kept as a module-level function (not a private method) because
|
|
463
|
+
* Cordis exposes the provider through a tracing proxy.
|
|
464
|
+
*/
|
|
465
|
+
async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo) {
|
|
466
|
+
let detachSession;
|
|
467
|
+
let detachAgent;
|
|
468
|
+
let agent;
|
|
469
|
+
let idleExit;
|
|
470
|
+
try {
|
|
471
|
+
agent = new HermesAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
|
|
472
|
+
// Pin THIS session to the hermes route. The route resolves through the
|
|
473
|
+
// async catalog (the memoized promise), so this is awaited before the first
|
|
474
|
+
// prompt — `selectForNextRequest` is the same per-session mechanism
|
|
475
|
+
// `session/selectModel` uses.
|
|
476
|
+
try {
|
|
477
|
+
const route = await runtimeInfo?.routeContext({ provider: agentOptions.provider, model: agentOptions.model });
|
|
478
|
+
if (route !== undefined) {
|
|
479
|
+
const selection = { provider: route.provider, model: route.model };
|
|
480
|
+
let picked = selection;
|
|
481
|
+
installModelSelection(agent.ctx, {
|
|
482
|
+
get current() { return picked; },
|
|
483
|
+
set current(next) { picked = next; },
|
|
484
|
+
assembled: undefined,
|
|
485
|
+
});
|
|
486
|
+
session.append("model/selection", selection);
|
|
487
|
+
trace(`session route pinned: ${route.provider}/${route.model}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
// Fail-soft: the boot-time default-model push remains the fallback.
|
|
492
|
+
trace(`session route pin failed: ${String(error)}`);
|
|
493
|
+
}
|
|
494
|
+
const commit = await setup?.(agent.ctx, agent);
|
|
495
|
+
commit?.commit();
|
|
496
|
+
await appendUnstoredSuffix(stored, session);
|
|
497
|
+
if (opts.enterSession) {
|
|
498
|
+
detachSession = agent.ctx.sessions.enter(session);
|
|
499
|
+
agent.ctx.sessions.announce(session);
|
|
500
|
+
}
|
|
501
|
+
detachAgent = loopCtx.agents.enter(agent, parentAgent);
|
|
502
|
+
loopCtx.agents.announce(agent);
|
|
503
|
+
emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
|
|
504
|
+
let disposed = false;
|
|
505
|
+
let unfollowOwner;
|
|
506
|
+
const dispose = async () => {
|
|
507
|
+
if (disposed)
|
|
508
|
+
return;
|
|
509
|
+
disposed = true;
|
|
510
|
+
trace(`dispose() called for agent ${id} (source=${source})`);
|
|
511
|
+
unfollowOwner?.();
|
|
512
|
+
await agent?.dispose();
|
|
513
|
+
detachAgent?.();
|
|
514
|
+
detachSession?.();
|
|
515
|
+
await stored.handle.close();
|
|
516
|
+
};
|
|
517
|
+
idleExit = () => {
|
|
518
|
+
void dispose().catch((error) => {
|
|
519
|
+
loopCtx.logger.warn(`hermes-provider: teardown of ${id} failed: ${String(error)}`);
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
unfollowOwner = ownerCtx.effect(() => () => {
|
|
523
|
+
void dispose().catch((error) => {
|
|
524
|
+
loopCtx.logger.warn(`hermes-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
|
|
525
|
+
});
|
|
526
|
+
}, `hermesProvider.lifecycle(${id})`);
|
|
527
|
+
return { agent, dispose };
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
|
|
531
|
+
detachAgent?.();
|
|
532
|
+
detachSession?.();
|
|
533
|
+
void agent?.dispose().catch(() => { });
|
|
534
|
+
client.close();
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
opts.preparation?.[Symbol.dispose]();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export default HermesProvider;
|