Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
if (event.payload.streaming) {
return `${message.text}${event.payload.text}`;
}
if (event.payload.replaceText) {
return event.payload.text;
}
if (event.payload.text.length === 0) {
return message.text;
}
Expand Down
132 changes: 132 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,138 @@ describe("ProviderRuntimeIngestion", () => {
expect(message?.streaming).toBe(false);
});

it("uses assistant completion detail as the authoritative final text", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "content.delta",
eventId: asEventId("evt-assistant-rewrite-delta"),
provider: ProviderDriverKind.make("hermes"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-rewrite"),
itemId: asItemId("item-rewrite"),
payload: {
streamKind: "assistant_text",
delta: "answer▌",
},
});
harness.emit({
type: "item.completed",
eventId: asEventId("evt-assistant-rewrite-completed"),
provider: ProviderDriverKind.make("hermes"),
createdAt: now,
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-rewrite"),
itemId: asItemId("item-rewrite"),
payload: {
itemType: "assistant_message",
status: "completed",
detail: "answer",
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.messages.some(
(message: ProviderRuntimeTestMessage) =>
message.id === "assistant:item-rewrite" && !message.streaming,
),
);
const message = thread.messages.find(
(entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-rewrite",
);
expect(message?.text).toBe("answer");
});

it("allows authoritative completion text to clear a stale streamed preview", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "content.delta",
eventId: asEventId("evt-assistant-clear-delta"),
provider: ProviderDriverKind.make("hermes"),
createdAt: now,
threadId: asThreadId("thread-1"),
itemId: asItemId("item-clear"),
payload: { streamKind: "assistant_text", delta: "stale preview" },
});
harness.emit({
type: "item.completed",
eventId: asEventId("evt-assistant-clear-completed"),
provider: ProviderDriverKind.make("hermes"),
createdAt: now,
threadId: asThreadId("thread-1"),
itemId: asItemId("item-clear"),
payload: {
itemType: "assistant_message",
status: "completed",
data: { finalText: "" },
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.messages.some(
(message: ProviderRuntimeTestMessage) =>
message.id === "assistant:item-clear" && !message.streaming,
),
);
const message = thread.messages.find(
(entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-clear",
);
expect(message?.text).toBe("");
});

it("projects structured assistant image attachments without requiring text", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";

harness.emit({
type: "item.completed",
eventId: asEventId("evt-assistant-image-completed"),
provider: ProviderDriverKind.make("hermes"),
createdAt: now,
threadId: asThreadId("thread-1"),
itemId: asItemId("item-image"),
payload: {
itemType: "assistant_message",
status: "completed",
data: {
attachments: [
{
type: "image",
id: "thread-1-image-attachment",
name: "result.png",
mimeType: "image/png",
sizeBytes: 128,
},
],
},
},
});

const thread = await waitForThread(harness.readModel, (entry) =>
entry.messages.some(
(message: ProviderRuntimeTestMessage) =>
message.id === "assistant:item-image" && !message.streaming,
),
);
const message = thread.messages.find(
(entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-image",
);
expect(message?.text).toBe("");
expect(message?.attachments).toEqual([
{
type: "image",
id: "thread-1-image-attachment",
name: "result.png",
mimeType: "image/png",
sizeBytes: 128,
},
]);
});

it("preserves completed tool metadata on projected tool activities", async () => {
const harness = await createHarness();
const now = "2026-01-01T00:00:00.000Z";
Expand Down
40 changes: 36 additions & 4 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
ApprovalRequestId,
type AssistantDeliveryMode,
ChatAttachment,
type ChatAttachment as ChatAttachmentType,
CommandId,
MessageId,
type OrchestrationEvent,
Expand All @@ -24,6 +26,7 @@ import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";

Expand All @@ -41,6 +44,25 @@ import { ServerSettingsService } from "../../serverSettings.ts";

const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`;
const isChatAttachment = Schema.is(ChatAttachment);

function attachmentsFromItemData(data: unknown): ReadonlyArray<ChatAttachmentType> | undefined {
if (typeof data !== "object" || data === null || !("attachments" in data)) {
return undefined;
}
const attachments = (data as { readonly attachments?: unknown }).attachments;
if (!Array.isArray(attachments)) {
return undefined;
}
const parsed = attachments.filter(isChatAttachment);
return parsed.length > 0 ? parsed : undefined;
}

function finalTextFromItemData(data: unknown): string | undefined {
if (typeof data !== "object" || data === null || !("finalText" in data)) return undefined;
const finalText = (data as { readonly finalText?: unknown }).finalText;
return typeof finalText === "string" ? finalText : undefined;
}

// Fallback when the in-memory description cache no longer has the task name
// (server restart, session-exit sweep, TTL/capacity eviction): earlier
Expand Down Expand Up @@ -694,9 +716,7 @@ const make = Effect.gen(function* () {
const projectionTurnRepository = yield* ProjectionTurnRepository;
const serverSettingsService = yield* ServerSettingsService;
const providerCommandId = (event: ProviderRuntimeEvent, tag: string) =>
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
Effect.succeed(CommandId.make(`provider:${event.eventId}:${tag}`));

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand Down Expand Up @@ -1006,6 +1026,8 @@ const make = Effect.gen(function* () {
commandTag: string;
finalDeltaCommandTag: string;
fallbackText?: string;
finalText?: string;
attachments?: ReadonlyArray<ChatAttachmentType>;
hasProjectedMessage?: boolean;
}) =>
Effect.gen(function* () {
Expand All @@ -1030,12 +1052,14 @@ const make = Effect.gen(function* () {
});
}

if (input.hasProjectedMessage || hasRenderableText) {
if (input.hasProjectedMessage || hasRenderableText || (input.attachments?.length ?? 0) > 0) {
yield* orchestrationEngine.dispatch({
type: "thread.message.assistant.complete",
commandId: yield* providerCommandId(input.event, input.commandTag),
threadId: input.threadId,
messageId: input.messageId,
...(input.finalText !== undefined ? { text: input.finalText } : {}),
...(input.attachments !== undefined ? { attachments: input.attachments } : {}),
...(input.turnId ? { turnId: input.turnId } : {}),
createdAt: input.createdAt,
});
Expand Down Expand Up @@ -1558,6 +1582,8 @@ const make = Effect.gen(function* () {
`assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
),
fallbackText: event.payload.detail,
finalText: finalTextFromItemData(event.payload.data) ?? event.payload.detail,
attachments: attachmentsFromItemData(event.payload.data),
}
: undefined;
const proposedPlanCompletion =
Expand Down Expand Up @@ -1609,6 +1635,12 @@ const make = Effect.gen(function* () {
...(assistantCompletion.fallbackText !== undefined && shouldApplyFallbackCompletionText
? { fallbackText: assistantCompletion.fallbackText }
: {}),
...(assistantCompletion.finalText !== undefined
? { finalText: assistantCompletion.finalText }
: {}),
...(assistantCompletion.attachments !== undefined
? { attachments: assistantCompletion.attachments }
: {}),
});

if (turnId) {
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
threadId: command.threadId,
messageId: command.messageId,
role: "assistant",
text: "",
text: command.text ?? "",
...(command.text !== undefined ? { replaceText: true } : {}),
...(command.attachments !== undefined ? { attachments: command.attachments } : {}),
turnId: command.turnId ?? null,
streaming: false,
createdAt: command.createdAt,
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/orchestration/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,11 @@ export function projectEvent(
...entry,
text: message.streaming
? `${entry.text}${message.text}`
: message.text.length > 0
: payload.replaceText
? message.text
: entry.text,
: message.text.length > 0
? message.text
: entry.text,
streaming: message.streaming,
updatedAt: message.updatedAt,
turnId: message.turnId,
Expand Down
56 changes: 56 additions & 0 deletions apps/server/src/provider/Drivers/HermesDriver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { assert, it } from "@effect/vitest";
import { HermesBridgeRequestId, ProviderInstanceId } from "@t3tools/contracts";

import { makeHermesProviderSnapshot } from "./HermesDriver.ts";

it("surfaces the active Hermes identity and canonical commands plus aliases", () => {
const snapshot = makeHermesProviderSnapshot({
instanceId: ProviderInstanceId.make("hermes"),
displayName: undefined,
accentColor: undefined,
enabled: true,
checkedAt: "2026-07-22T00:00:00.000Z",
capabilities: {
protocolVersion: 1,
requestId: HermesBridgeRequestId.make("capabilities"),
capabilities: {
asynchronousDelivery: true,
imageAttachments: true,
interrupts: true,
approvals: true,
clarifications: true,
slashConfirmations: true,
threadCreation: true,
commandCatalog: true,
},
provider: "openrouter",
model: "anthropic/claude-sonnet-4",
profile: "default",
commands: [
{
name: "new",
description: "Start a new conversation",
inputHint: "[name]",
aliases: ["reset"],
},
],
},
});

assert.equal(snapshot.status, "ready");
assert.equal(snapshot.auth.label, "default");
assert.deepEqual(snapshot.models, [
{
slug: "anthropic/claude-sonnet-4",
name: "anthropic/claude-sonnet-4",
subProvider: "openrouter",
isCustom: true,
isDefault: true,
capabilities: null,
},
]);
assert.deepEqual(
snapshot.slashCommands.map(({ name }) => name),
["new", "reset"],
);
});
Loading