Documentation
¶
Overview ¶
Package textappend implements the BUILD-phase "text-append" plugin described in uiux-research/plugin-b-c-spec.md §7.2, with the R1 streaming redesign (append-to-last-delta, not new-block-synthesis).
One plugin instance can serve BOTH directions: Up.Suffix is appended to the last user message (or merged into the system prompt) before forwarding; Down.Suffix is appended to the assistant content (or reasoning) before the client sees it. Either side may be empty.
Target semantics:
Up.Target = "last_user_message" → append to the trailing user turn's
text content
Up.Target = "system_prompt" → append to ParsedRequest.SystemPrompt
Down.Target = "content" → append to ParsedResponse.Content
Down.Target = "reasoning" → append to ParsedResponse.Reasoning
Streaming-AFTER for Down.Target = "content" registers an OnLastTextDelta callback. The framework's StreamDispatcher buffers one event of lookahead per content block and fires the callback on the LAST text_delta of each content block, producing a wire sequence that ends ...text_delta(text+suffix), content_block_stop, ..., message_stop — with no synthesized event after the protocol's terminator. Streaming-AFTER for Down.Target = "reasoning" is a v1 no-op (the dispatcher has no reasoning-last-delta hook); operators wanting reasoning suffixes use non-streaming responses.
Block-index policy: the callback is registered as index-agnostic (fires on every text content block). The plain "last block only" reading of the task spec was rejected because Anthropic extended- thinking responses put thinking at content_block index 0 and the assistant text at index 1; a strict `idx == 0` gate would skip the suffix on every thinking-enabled response. Consequence: responses that emit MULTIPLE text content blocks (rare but legal — e.g. a tool_use sandwiched between text blocks) get the suffix appended once per text block. Operators who specifically need "only the first text block" gate on their own logic via a chained OnLastTextDelta (the block index is passed to the callback).
Probability lets operators dial trigger frequency for easter-egg modes. Absent (nil) means 1.0 ("always"); 0.0 means "never". Each hook call draws a math/rand/v2 float; the suffix lands only when the draw is below Probability.
Index ¶
Constants ¶
const ( TargetLastUserMessage = "last_user_message" TargetSystemPrompt = "system_prompt" TargetContent = "content" TargetReasoning = "reasoning" )
Target constants are stable wire strings. New values land here once the spec freezes them; do not invent variants ad-hoc.
const Type = "text-append"
Type is the operator-facing type name registered with v2.RegisterBuiltin.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
Routes []string `json:"routes"`
Up UpRule `json:"up"`
Down DownRule `json:"down"`
Probability *float64 `json:"probability,omitempty"`
}
Config is the parsed instance config.
Probability is a pointer so absent in JSON is distinguishable from 0.0 (which would mean "never trigger"). Valid range is [0.0, 1.0]; values outside the range fail Init.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin implements both v2.BeforePlugin and v2.AfterPlugin. Either side is a no-op when its Suffix is empty.
func New ¶
New parses cfg, applies target defaults, validates enums, and returns a ready Plugin. The default RNG is math/rand/v2.Float64; tests use NewWithRand to inject a deterministic source.
func NewWithRand ¶
NewWithRand is the test seam: lets a unit test supply a deterministic rand.Float64 stand-in so probability assertions don't depend on the process-global RNG. Production callers use New.
func (*Plugin) ConfigSchema ¶
ConfigSchema describes the per-instance form for the viewer Settings panel (W4). See textreplace for the W3-canonicalization note.
func (*Plugin) OnAfter ¶
func (p *Plugin) OnAfter(_ context.Context, req *v2.ParsedRequest, ac *v2.AfterContext, _ map[string]any) v2.AfterResult
OnAfter behaves per ac.Response branch:
Streaming (ac.Response == nil) with Down.Target == "content": register an OnLastTextDelta callback. The dispatcher buffers one event of lookahead per content block and fires the callback on the LAST text_delta of the first text block (block index 0 by default). The wire sequence ends with the mutated delta, then the protocol's per-block terminator, then any tool_use blocks, then the protocol's overall terminal — no synthesized event follows the protocol's terminator (this is the R1 fix vs. EmitBeforeFinish, which appended a new block after the terminator).
Streaming with Down.Target == "reasoning": no-op (dispatcher does not expose a reasoning-last-delta hook in v1). Operators wanting reasoning suffixes use non-streaming responses.
Non-streaming: append the suffix to ac.Response.Content (or .Reasoning) and return ActionMutate. Shallow clone is safe — only string fields move.
Returns ActionContinue when the route does not match, Down.Suffix is empty, or the per-call probability draw does not fire. The non-streaming branch always Mutates when those checks pass — append is unconditional and operators who chain instances see additive suffixes (idempotence is not promised).
Probability note: in the streaming branch the draw happens at hook registration time, not per-event — the callback either runs or doesn't for the whole stream. This matches operator intent for easter-egg mode: a stream either gets the footer or doesn't.
func (*Plugin) OnBefore ¶
func (p *Plugin) OnBefore(_ context.Context, req *v2.ParsedRequest, _ map[string]any) v2.BeforeResult
OnBefore appends Up.Suffix to either the trailing user turn's text or to SystemPrompt, per Up.Target. Returns ActionContinue (no rebuild) when:
- the route does not match
- Up.Suffix is empty
- target == last_user_message but no user turn carries text content
- the per-call probability draw does not fire (operator-configured easter-egg mode; default Probability nil = always fires)
type Schema ¶
type Schema struct {
Fields []SchemaField `json:"fields"`
}
Schema and SchemaField mirror the textreplace stand-in. See that package for the W3-canonicalization note.