Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { moveComputeFile } from "../../../impl/moveComputeFile.js";
import { iterateDiffContentLines } from "../../../impl/iterateDiffContentLines.js";
import { resolveComputePath } from "../../../impl/resolveComputePath.js";
import { writeComputeTextFile } from "../../../impl/writeComputeTextFile.js";
import { parseCodexPatch, type CodexPatchHunk } from "./parseCodexPatch.js";
import { parseCodexPatch, type CodexPatchHunk } from "../../../../impl/parseCodexPatch.js";

/** One file the patch changed, named both as the patch wrote it and as the machine sees it. */
export interface CodexPatchChange {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { resolveComputePath } from "../../../impl/resolveComputePath.js";
import { parseCodexPatchDirective } from "./parseCodexPatch.js";
import { parseCodexPatchDirective } from "../../../../impl/parseCodexPatch.js";

/**
* Every file a patch names, as absolute paths on the machine.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type HappySessionEvent =
// and a truer-looking event does not.
| { t: "service"; text: string }
| { t: "text"; text: string; thinking?: boolean }
| { t: "tool-call-end"; call: string }
| { t: "tool-call-end"; call: string; result?: string; isError?: boolean }
| {
t: "tool-call-start";
args: Record<string, unknown>;
Expand Down
13 changes: 13 additions & 0 deletions packages/happy-agent-modules/sources/happy/LEARNINGS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Happy module learnings

## Mobile tool wire normalization

- Normalize tool calls at the Happy sync boundary only when the mobile app already owns the same
semantic renderer and argument contract. Preserve every other real tool name and send a precise
activity description through the generic canonical tool-call envelope; a familiar but false
tool shape is worse than an honest generic row.
- Parse Codex `apply_patch` text inside Happy Agent and send the established mobile
`CodexPatch { changes }` payload. Mobile clients should render structured file changes and must
never need to parse Codex's patch grammar.
- Send Codex update hunks as `modify { old_content, new_content }`, which mobile already routes
through the same paired, intra-line diff renderer as Claude `Edit`. A raw unified patch sends
native mobile down its simpler prefix-colored fallback instead.

## Pairing and public state

- Resolve the Happy CLI home and server URL through `ConfigModule` even before credentials exist. Reading `process.env` directly bypasses daemon-owned environment overrides and can accidentally inspect another installation during hermetic tests.
Expand Down
57 changes: 31 additions & 26 deletions packages/happy-agent-modules/sources/happy/mapHappyMessages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Type } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";

import type { AgentEvent } from "../events/index.js";
import type { HistoryMessage } from "../history/index.js";
import type {
Expand All @@ -9,6 +8,7 @@ import type {
HappySessionProtocolMessage,
HappyUsage,
} from "./HappyProtocol.js";
import { happyToolCallPresentation, normalizeHappyToolCall } from "./normalizeHappyToolCall.js";

/** How many event ids are remembered so a replayed event is not shown twice. */
const MAX_REMEMBERED_EVENTS = 16_384;
Expand Down Expand Up @@ -70,7 +70,11 @@ const toolEndSchema = Type.Object(
rigEvent: Type.Object(
{
result: Type.Object(
{ toolCallId: Type.String({ minLength: 1 }) },
{
display: Type.Optional(Type.String()),
isError: Type.Optional(Type.Boolean()),
toolCallId: Type.String({ minLength: 1 }),
},
{ additionalProperties: true },
),
type: Type.Literal("tool_execution_end"),
Expand Down Expand Up @@ -253,7 +257,7 @@ export class HappyMessageMapper {
if (block.type === "tool_result") {
output.push(
this.#createMessage({
ev: this.#toolResultEvent(block.callId),
ev: this.#toolResultEvent(block.callId, block.display, block.isError),
id,
role: "agent",
time,
Expand Down Expand Up @@ -322,9 +326,13 @@ export class HappyMessageMapper {
];
}
if (Value.Check(toolEndSchema, event.payload)) {
const callId = event.payload.rigEvent.result.toolCallId;
const result = event.payload.rigEvent.result;
return [
this.#agentMessage(event, `tool-result:${callId}`, this.#toolResultEvent(callId)),
this.#agentMessage(
event,
`tool-result:${result.toolCallId}`,
this.#toolResultEvent(result.toolCallId, result.display, result.isError),
),
];
}
return [];
Expand All @@ -335,19 +343,30 @@ export class HappyMessageMapper {
id: string;
name: string;
}): Extract<HappySessionEvent, { t: "tool-call-start" }> {
const title = humanizeToolName(call.name);
const args = call.arguments === undefined ? {} : toRecord(call.arguments);
const normalized = normalizeHappyToolCall(call.name, args);
const presentation = happyToolCallPresentation(call.name, normalized);
return {
args: call.arguments === undefined ? {} : toRecord(call.arguments),
args: normalized.args,
call: call.id,
description: `Running ${title}`,
name: call.name,
description: presentation.description,
name: normalized.name,
t: "tool-call-start",
title,
title: presentation.title,
};
}

#toolResultEvent(callId: string): Extract<HappySessionEvent, { t: "tool-call-end" }> {
return { call: callId, t: "tool-call-end" };
#toolResultEvent(
callId: string,
result?: string,
isError?: boolean,
): Extract<HappySessionEvent, { t: "tool-call-end" }> {
return {
call: callId,
...(result === undefined ? {} : { result }),
...(isError === true ? { isError: true } : {}),
t: "tool-call-end",
};
}

#mapInference(event: AgentEvent): readonly HappySessionProtocolMessage[] {
Expand Down Expand Up @@ -473,20 +492,6 @@ export class HappyMessageMapper {
}
}

/** Turns a tool's identifier into the words a person reads on the phone. */
function humanizeToolName(value: string): string {
const spaced = value
.replaceAll(/[_-]+/gu, " ")
.replaceAll(/([a-z])([A-Z])/gu, "$1 $2")
.trim();
return spaced.length === 0
? "Tool"
: spaced
.split(/\s+/u)
.map((part) => part[0]!.toUpperCase() + part.slice(1))
.join(" ");
}

function toRecord(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
Expand Down
Loading