textappend

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: MIT Imports: 6 Imported by: 0

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

View Source
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.

View Source
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 DownRule

type DownRule struct {
	Suffix string `json:"suffix"`
	Target string `json:"target"`
}

DownRule controls what AFTER appends and where.

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

func New(cfg map[string]any) (*Plugin, error)

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

func NewWithRand(cfg map[string]any, r randFunc) (*Plugin, error)

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

func (p *Plugin) ConfigSchema() Schema

ConfigSchema describes the per-instance form for the viewer Settings panel (W4). See textreplace for the W3-canonicalization note.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the stable type identifier.

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.

type SchemaField

type SchemaField struct {
	Name        string   `json:"name"`
	Label       string   `json:"label"`
	Type        string   `json:"type"`
	Default     string   `json:"default,omitempty"`
	Enum        []string `json:"enum,omitempty"`
	Description string   `json:"description,omitempty"`
}

type UpRule

type UpRule struct {
	Suffix string `json:"suffix"`
	Target string `json:"target"`
}

UpRule controls what BEFORE appends and where.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL