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
1 change: 1 addition & 0 deletions docs/architecture/agent-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ The [shared title contracts](../../packages/shared/src/types/api/conversation-ti
- [`@agenetes/agentlet-host`](../../external/agenetes/packages/agentlet-host) mounts the durably stateless [`@agenetes/agentlet-gateway`](../../external/agenetes/packages/agentlet-gateway), supervises the local agentlet daemon, and injects host-owned authentication. The Gateway owns only live control/session connections, pending RPCs, reconnect buffers, and bounded pre-attach buffering; durable workload and conversation state remains in Agenetes. Ordinary control RPCs time out after 60 seconds, while `server/spawn` has a separate 240-second deadline because it includes ACP `initialize` plus session lifecycle bootstrap, whose two sequential requests may each take up to 90 seconds.
- [`@agenetes/agent-profile`](../../external/agenetes/packages/agent-profile) owns ordinary Profile schemas, CRUD and persistence, without Team discovery, setup or Config dependencies. `buildAcpWorkloadSpec()` reads the selected Profile and snapshots its explicit placement, command and working directory before Agenetes creation. Retired manifest Profiles are not selectable or compiled into executable recipes.
- [`@agenetes/acp-driver`](../../external/agenetes/packages/acp-driver) owns the canonical ACP spec/state schemas, session creation/resume, canonical-input flattening, ACP update translation, and durable state up-reporting. The static DriverMap binds `external` directly to this driver. Generic runtime-environment hooks remain available, but Huabu no longer injects manifest Configs or recovers Team recipes. Retired Team recipes are explicitly rejected rather than silently reinterpreted as ordinary commands. Live spawn and session caches are isolated by `(agentletId, threadId)`, and unavailable targets fail with `placement_unavailable`. Because ACP has no native system instruction channel, the driver prefixes joined `AgentSpec.initialPreamble` fragments to the first ordinary prompt. A first control causes the host to ensure the session from the canonical spec before calling `handle.control()`; it creates no Chat-V2 turn and does not consume the pending preamble. Session control state is deliberately split in two: the agent-reported surface (`currentModeId` / `currentModelId` / `configOptions[].currentValue`) and `selections`, a map of explicit per-thread user choices keyed by config-option id (`mode`, `model`, and agent-defined ids such as `allow_all`). Only a successful `set_mode` / `set_model` / `set_config_option` writes `selections`; agent pushes never do, because agents such as Copilot CLI implement config options as process-global user settings and broadcast one value to every live session, making the agent-reported value answer "what was picked last, anywhere" rather than "what was picked for this thread". `selections` travels with the rest of `AgentMetadata` and is the authoritative per-thread intent. On resume it is restored unconditionally and replayed onto the agent knob by knob before prompts or user controls proceed. A rejected knob is forgotten only when the agent definitively refuses it, so a retired model id cannot wedge the thread while a transport failure cannot destroy durable intent.
- If an ACP turn ends without an `agent_message_chunk`, the driver preserves its tool, thinking, and terminal events and completes with an empty message; it never synthesizes diagnostic prose into the assistant transcript.
- External-agent idle suspension is host policy: General Settings persists `idleTimeoutSecs` (10 minutes by default, `0` disables suspension), and Huabu injects the current value when a new or resumed ACP process is spawned. Agentlet never suspends a session while a host JSON-RPC request remains in flight; transport teardown closes the ACP client so pending prompts reject and clean up immediately. The long-lived `AcpAgentHandle` self-repairs a suspended lower-level session lazily on the next turn. Direct driver controls still require a live session, so Huabu's control route first ensures or resumes that session from the canonical persisted spec and then calls `handle.control()`.
- ACP has no native seam for injecting prior assistant messages, so when native resume is unavailable the driver replays history as one prepended text block. It first projects every durable turn through `projectTextHistoryTurn` (`@agenetes/runtime`), which replaces image bodies with a short placeholder — a base64 payload carries no meaning once flattened into text, and inlining it would only inflate the payload. The _projected_ turns are what gets authorized, so the admission estimate prices the block that is actually sent.
- Opening Chat and opening the slash menu read only `GET /api/acp/threads/:threadId/cached-meta`. The response projects cached slash commands and selector catalogues from a live or persisted realized thread first, then from `profile-schema-cache`, and finally returns a successful empty observation. These reads never call `agenetes.create()`, spawn ACP, or create a WorkloadSpec. Profile-level mode/model values may be displayed as last observed; generic config-option values render without a selected value until the current thread reports them or records a successful explicit choice. Live metadata continues updating the current thread and is folded back into the Profile cache. Huabu separately remembers only successful explicit model and `thought_level` choices in the Profile's host-owned `customData`; modes, auto-approve/full-access controls, booleans, and unknown config options remain thread-only.
Expand Down
13 changes: 0 additions & 13 deletions external/agenetes/packages/acp-driver/src/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,19 +642,6 @@ export class AcpAgentHandle<
resolveWaiter = resolve;
});
}

// Visibility fallback for "empty" turns. External agents can finish
// a turn with zero text (e.g. a tool-only Read/Glob/Bash chain).
// Synthesize a single explanatory `text_delta` whenever the agent
// produced no text AND we're not about to surface an error or abort,
// so the UI doesn't look hung.
const aborted = signal?.aborted ?? false;
if (assembledText.length === 0 && !promptError && !aborted) {
const reason = stopReason ?? 'unknown';
const synthetic = `_(agent returned no text — stopReason: ${reason}. Usually a tool-only turn or a refusal without prose. Extend the ACP translator if you need tool-call rendering.)_`;
assembledText = synthetic;
yield { type: 'text_delta', data: { content: synthetic } };
}
} finally {
// Commit the turn's plan (full-replacement; latest wins) into the
// route-owned `overlay` for the live sidecar. The durable transcript's
Expand Down
31 changes: 31 additions & 0 deletions external/agenetes/packages/acp-driver/src/recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,37 @@ describe('ACP durable history recovery', () => {
expect(sessionMocks.reportEntryState).not.toHaveBeenCalled();
});

it('does not synthesize user-visible text for an empty turn', async () => {
const { entry } = sessionEntry();
sessionMocks.ensureAcpSession.mockResolvedValue(entry);
const handle = new AcpAgentHandle(spec, {
recovery: {
authorizeHistoryLoad: vi.fn(async () => ({
allowed: true as const,
estimatedSize: 0,
})),
},
});

const events = [];
for await (const event of handle.run(submission, {
overlay: emptyAcpOverlay(),
logger,
})) {
events.push(event);
}

expect(events).toEqual([
{
type: 'done',
data: {
message: '',
meta: { stopReason: 'end_turn' },
},
},
]);
});

it('persists a command-created session without consuming its preamble', async () => {
const { entry, prompt } = sessionEntry();
sessionMocks.ensureAcpSession.mockResolvedValue(entry);
Expand Down
Loading