History

feat: surface the Hermes model on instance status

`connection.hello` gained an optional `model` field, but the value died in the broker: `HermesGatewayInstanceStatus` carried pluginVersion and hermesVersion with nowhere for the model to go, so the provider picker had no way to name the model Hermes actually runs. Add `model` to the status struct as a required nullable field. Null covers both "no plugin has connected yet" and "the connected plugin predates the field" — the plugin ships separately from the server, so a plugin that never sends it must still complete the handshake rather than failing the frame decoder. Tests cover the present and absent paths on both the hello frame and the status projection. The model is volatile liveness, not durable config: it belongs in broker memory alongside pluginVersion/hermesVersion/capabilities and is never persisted to settings.

Ben Davis committed 3 months ago

Commit e8be9671 parent 5b519772 files · +44 −0Browse files

Unified · Split

2 files
  1. packages/contracts/src/hermesGateway.test.ts +37 −0
  2. packages/contracts/src/hermesGateway.ts +7 −0
packages/contracts/src/hermesGateway.test.ts+37−0
@@ -69,6 +69,7 @@ describe("Hermes gateway management contracts", () => { · 68 unmodified lines
6969 lastConnectedAt: "2026-07-23T12:00:00.000Z",
7070 pluginVersion: "0.2.0",
7171 hermesVersion: "1.2.3",
72+ model: "gpt-5.6-terra",
7273 activeSessionCount: 2,
7374 protocolVersion: 2,
7475 capabilities: {
@@ -82,6 +83,12 @@ describe("Hermes gateway management contracts", () => { · 7 unmodified lines
8283 });
8384 expect(connected.status).toBe("connected");
8485 expect(connected.activeSessionCount).toBe(2);
86+ expect(connected.model).toBe("gpt-5.6-terra");
87+
88+ // A plugin that predates the `model` field still produces a valid status;
89+ // the picker falls back to the generic label rather than failing to decode.
90+ const withoutModel = decodeInstanceStatus({ ...connected, model: null });
91+ expect(withoutModel.model).toBeNull();
8592
8693 const upgradeRequired = decodeInstanceStatus({
8794 ...connected,
@@ -134,6 +141,7 @@ describe("Hermes gateway handshake", () => { · 46 unmodified lines
134141 userInput: true,
135142 attachments: false,
136143 },
144+ model: "gpt-5.6-terra",
137145 authentication: {
138146 type: "instance-credential",
139147 instanceId: "hermes-research",
@@ -142,6 +150,35 @@ describe("Hermes gateway handshake", () => { · 2 unmodified lines
142150 });
143151
144152 expect(hello.authentication.type).toBe("instance-credential");
153+ expect(hello.model).toBe("gpt-5.6-terra");
154+ });
155+
156+ // The plugin ships separately from the server, so a plugin that predates the
157+ // `model` field must still complete the handshake rather than failing the
158+ // frame decoder. T3 falls back to the generic model label.
159+ it("accepts a hello from a plugin that reports no model", () => {
160+ const hello = decodeHello({
161+ type: "connection.hello",
162+ requestId: "hello-no-model",
163+ protocolVersion: 2,
164+ pluginVersion: "0.2.0",
165+ hermesVersion: "1.2.3",
166+ capabilities: {
167+ protocolVersion: 2,
168+ streaming: true,
169+ activity: true,
170+ approvals: true,
171+ userInput: true,
172+ attachments: false,
173+ },
174+ authentication: {
175+ type: "instance-credential",
176+ instanceId: "hermes-research",
177+ credential: "persistent-secret",
178+ },
179+ });
180+
181+ expect(hello.model).toBeUndefined();
145182 });
146183
147184 it("decodes a future-version hello so the broker can reject it explicitly", () => {
packages/contracts/src/hermesGateway.ts+7−0
@@ -123,6 +123,13 @@ export const HermesGatewayInstanceStatus = Schema.Struct({ · 122 unmodified lines
123123 lastConnectedAt: Schema.NullOr(IsoDateTime),
124124 pluginVersion: Schema.NullOr(TrimmedNonEmptyString),
125125 hermesVersion: Schema.NullOr(TrimmedNonEmptyString),
126+ /**
127+ * The model the connected plugin reported at handshake, surfaced so the
128+ * provider picker can name the model Hermes actually runs instead of a
129+ * placeholder. Null when no plugin has connected yet, or when the connected
130+ * plugin predates the `model` field on `connection.hello`.
131+ */
132+ model: Schema.NullOr(TrimmedNonEmptyString),
126133 activeSessionCount: NonNegativeInt,
127134 protocolVersion: Schema.NullOr(PositiveInt),
128135 capabilities: Schema.NullOr(HermesGatewayCapabilities),