Documentation
¶
Overview ¶
Package handler turns inbound Zulip events into ACP prompts and streams the agent's answer back into the originating topic.
The unit of conversation is a Zulip TOPIC, scoped by its channel. The topic string itself is never used as a key — see internal/journal for why — so everything here works in terms of the journal's stable conv-id.
Index ¶
Constants ¶
const InterruptedMarker = "\n\n*(relay restarted — turn interrupted)*"
InterruptedMarker is appended to a tail message the relay was still streaming into when it stopped. A restart kills the child agent, so the turn is dead regardless; the correct behaviour is to say so and let the next message start cleanly, never to try to continue a half-streamed message.
const OutboxDir = "outbox"
OutboxDir is the per-conversation directory an agent writes files into to have them uploaded to Zulip. Relative to the session cwd.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Agent ¶
type Agent interface {
Prompt(ctx context.Context, sid acp.SessionId, prompt []acp.ContentBlock) (acp.StopReason, error)
Models() (models []client.ModelInfo, currentID string)
}
Agent is the subset of *client.AgentProc the handler drives directly. Session lifecycle lives in Sessions.
type Config ¶
type Config struct {
Client Poster
Agent Agent
Sessions Sessions
Journal *journal.Journal
// BotUserID is the relay's own Zulip user id. Messages from it are
// refused unconditionally: the relay must never feed its own
// output back into the agent.
BotUserID int64
// BotFullName is the bot's display name, used to recognise
// @-mentions in raw markdown.
BotFullName string
// BotSenderIDs are user ids whose messages are never treated as a
// human turn: every bot in the realm, snapshotted at startup. A
// bot that appears later is not in the set, but the cross-realm
// system bots — the ones that actually post unprompted — are
// caught by the SenderRealm check instead.
BotSenderIDs map[int64]struct{}
// Channels is the resolved allowlist: stream id → channel name.
// The relay answers nowhere else.
Channels map[int64]string
// AllowedUsers, if non-nil, restricts who the relay answers.
AllowedUsers map[int64]struct{}
// PromptTimeout caps one agent turn.
PromptTimeout time.Duration
// EditInterval coalesces streaming edits.
EditInterval time.Duration
// Budget, SealMarker and ContinuationMarker configure the splitter.
Budget int
SealMarker string
ContinuationMarker string
// SilentSentinel lets the agent decline to answer an ambient turn.
SilentSentinel string
// HideThinking suppresses thought chunks on the streamed path.
HideThinking bool
// Logf receives operational messages.
Logf func(format string, args ...any)
}
Config configures a Handler.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler implements the event side of the relay.
func (*Handler) Handle ¶
func (h *Handler) Handle(ctx context.Context, ev zulipproto.Event)
Handle is the zulipproto.EventHandler entry point.
func (*Handler) MarkInterrupted ¶
MarkInterrupted annotates every message the relay was streaming into when it last stopped. A restart kills the child agent, so the turn is dead; saying so beats leaving a truncated answer that looks complete.
It never attempts to continue a half-streamed message.
type Poster ¶
type Poster interface {
SendMessage(ctx context.Context, streamID int64, topic, content string) (int64, error)
EditMessage(ctx context.Context, id int64, content string) error
GetMessage(ctx context.Context, id int64) (zulipproto.Message, error)
Upload(ctx context.Context, filename string, r io.Reader) (string, error)
}
Poster is the Zulip surface the handler writes to.
type Sessions ¶
type Sessions interface {
GetOrCreate(ctx context.Context, key string, sink client.SessionUpdateSink) (*state.Session, error)
Touch(s *state.Session)
Cancel(ctx context.Context, key string)
TakePendingSystemPrompt(s *state.Session) string
}
Sessions is the subset of *state.Manager the handler uses.