threadcase

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package threadcase hosts the thread-mode agent: a plan-and-execute turn (planexec.Runner) that runs when a Case is created from a monitored channel post (materialize the Case fields) or when the bot is mentioned in a Case thread (investigate and respond / update fields / close). Slack SDK imports are forbidden here; the host communicates via the Handler interface and the returned Decision, exactly like casebound / proposal.

Package threadcase hosts the thread-mode case agents: the one that materialises a new Case from the conversation that triggered it, and the one that answers a mention on an existing Case. Both are plan-and-execute agents running on the agentkit runtime.

A turn is a durable process: StartTurn spawns it and returns, and its decision is applied by the completion handler through the Host port. Slack SDK imports are forbidden here; the usecase layer owns the Slack service and i18n.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ConversationMessage

type ConversationMessage struct {
	Timestamp string
	UserID    string
	UserName  string
	Text      string
}

ConversationMessage is a single pre-fetched thread message handed to the runtime. The host resolves user display names; the runtime only formats.

type CreateDecision

type CreateDecision struct {
	Title       string          `json:"title" description:"A concise case title summarising the thread." required:"true"`
	Description string          `json:"description" description:"A clear case description derived from the thread and your investigation." required:"true"`
	Fields      []DecisionField `` /* 134-byte string literal not displayed */
}

CreateDecision is the structured final output of a ModeCreate turn (Run[CreateDecision]): the title / description / custom fields the planner wants the new case to carry. It reuses DecisionField (shared with the materialize decision). The planner schema is derived from these struct tags via gollem.ToSchema; Validate enforces the shape invariants. Workspace-schema field validation (required / options / types) is applied by the host when it commits the case (validateCreateDecision), not here — the value type has no access to the field schema.

func (CreateDecision) Validate

func (d CreateDecision) Validate() error

Validate enforces the create decision's shape invariants so a title-less or description-less proposal is rejected inside planexec's Run[CreateDecision] regeneration loop. It satisfies planexec.Validatable. Field-value validity is checked by the host against the workspace schema (validateCreateDecision).

type CreatePayload

type CreatePayload struct {
	Title       string
	Description string
	Fields      map[string]model.FieldValue
}

CreatePayload is handed to Handler.Create when the ModeCreate planner commits a new case. Fields are the already type-validated custom field values (Type injected, options/required checked by the runtime). The host owns the case identity (workspace / channel / thread / reporter) — those are captured when the host builds the Handler, not carried here.

type Decision

type Decision struct {
	Kind        DecisionKind    `` /* 300-byte string literal not displayed */
	Message     string          `json:"message,omitempty" description:"For respond: the reply text shown to the user. Omit for materialize."`
	Title       string          `json:"title,omitempty" description:"For materialize: a concise case title summarising the thread."`
	Description string          `json:"description,omitempty" description:"For materialize: a clear case description derived from the thread."`
	Fields      []DecisionField `json:"fields,omitempty" description:"For materialize: custom field assignments. Only include fields you are confident about."`
}

Decision is the structured final output of a mention turn (Run[Decision]). The schema handed to the planner is derived from these struct tags via gollem.ToSchema; Validate enforces the per-kind invariants the schema cannot (a plain JSON schema cannot say "materialize requires title + description").

func (Decision) Validate

func (d Decision) Validate() error

Validate enforces the mention decision's per-kind invariants so a malformed terminal output is rejected inside planexec's Run[Decision] regeneration loop rather than producing an empty reply or a blank materialize. It satisfies planexec.Validatable.

type DecisionField

type DecisionField struct {
	FieldID string   `json:"field_id" description:"The field id from the workspace schema." required:"true"`
	Value   string   `json:"value,omitempty" description:"Scalar value (text / number / url / single select option id)."`
	Values  []string `json:"values,omitempty" description:"Multi-select option ids."`
}

DecisionField is one custom-field assignment emitted by a materialize decision. Value carries the scalar form (text / number / url / single select); Values carries the multi-select form. The host maps field_id to the workspace field schema to build the typed FieldValue.

type DecisionKind

type DecisionKind string

DecisionKind discriminates the terminal action a mention turn resolves to. It is the `kind` field of the structured final output (Run[Decision]).

Closing / transitioning the case is NOT a Decision kind: that is a side effect the sub-agent performs during investigation via the case__update_case_status tool. The terminal Decision only covers the two host-applied outcomes — reply to the user, or materialize case content.

const (
	// DecisionRespond posts Message as a reply in the case thread.
	DecisionRespond DecisionKind = "respond"
	// DecisionMaterialize writes Title / Description / Fields onto the Case
	// (the host applies it via CaseUC.MaterializeThreadCase).
	DecisionMaterialize DecisionKind = "materialize"
)

type Durable added in v0.3.0

type Durable struct {
	// contains filtered or unexported fields
}

Durable runs the thread-mode agents on the agentkit runtime: one plan-execute agent for a mention on an existing case, and one for materialising a new case from the conversation that triggered it.

It coexists with the in-process planexec runner: a deployment that has not wired this keeps taking UseCase.RunTurn's synchronous path.

func NewDurable added in v0.3.0

func NewDurable(repo interfaces.Repository, registry *model.WorkspaceRegistry,
	host Host, locator agentkernel.Locator, models agentkernel.ModelPolicy,
) (*Durable, error)

NewDurable builds the durable thread-mode host. The registry is required because the create turn validates its proposed field values against the workspace schema, and a durable run resolves that schema from its own scope rather than from a value captured at spawn.

locator is used only to tell a re-delivered Slack event from a busy thread; a nil locator makes every delivery look fresh, which the idempotency key still covers.

func (*Durable) Bind added in v0.3.0

func (d *Durable) Bind(k *agentkit.Kernel, probe *agentkernel.ToolSetProbe)

Bind hands over the Kernel the registered agents run on, and the probe that tells this host which toolset ids actually resolve to a tool for a given run.

func (*Durable) Register added in v0.3.0

func (d *Durable) Register(
	reg *agentkit.Registry, taskAgent agentkit.Agent[react.Input],
	progress planexec.Progress, limiter agentkit.Limiter, store agentkit.HistoryStore,
) error

Register registers both thread-mode agents and wires this host as their completion handler. Call it before building the Kernel, and Bind after.

func (*Durable) StartTurn added in v0.3.0

func (d *Durable) StartTurn(ctx context.Context, req TurnRequest) (*Result, error)

StartTurn spawns one thread-mode turn and returns as soon as the run is recorded. Its decision is applied by the completion handler.

type Handler

type Handler interface {
	// TraceAppend records a milestone line that must stay visible (planner
	// rounds, task results) in the thread-side trace block.
	TraceAppend(ctx context.Context, line string)
	// TraceReplace overwrites the single transient activity line in place, so
	// per-tool chatter ("Searching…", "Fetching…") does not accumulate.
	TraceReplace(ctx context.Context, line string)
	// Question posts a question to the user. Returning an error aborts the turn.
	Question(ctx context.Context, ssn *model.Session, q QuestionPayload) error
	// Create persists a new case for the ModeCreate flow and returns it. The host
	// invokes it once, AFTER the turn completes, with the field values the
	// in-loop finalizer already validated against the workspace schema. A returned
	// error is a persistence failure the model cannot repair by re-emitting JSON,
	// so it is NOT fed back for regeneration; the host surfaces it and falls back.
	// Only called in ModeCreate turns.
	Create(ctx context.Context, ssn *model.Session, p CreatePayload) (*model.Case, error)
}

Handler is the host-side surface for one thread-mode turn. TraceAppend / TraceReplace render progress lines; Question posts a question to the thread; Create commits a new case for the ModeCreate flow.

type HandlerFuncs

type HandlerFuncs struct {
	TraceAppendFn  func(ctx context.Context, line string)
	TraceReplaceFn func(ctx context.Context, line string)
	QuestionFn     func(ctx context.Context, ssn *model.Session, q QuestionPayload) error
	CreateFn       func(ctx context.Context, ssn *model.Session, p CreatePayload) (*model.Case, error)
}

HandlerFuncs is a struct-of-funcs adapter for tests and minimal hosts. Missing entries are treated as no-ops (Create errors when unset, since a ModeCreate turn cannot commit without it).

func (HandlerFuncs) Create

func (h HandlerFuncs) Create(ctx context.Context, ssn *model.Session, p CreatePayload) (*model.Case, error)

func (HandlerFuncs) Question

func (h HandlerFuncs) Question(ctx context.Context, ssn *model.Session, q QuestionPayload) error

func (HandlerFuncs) TraceAppend

func (h HandlerFuncs) TraceAppend(ctx context.Context, line string)

func (HandlerFuncs) TraceReplace

func (h HandlerFuncs) TraceReplace(ctx context.Context, line string)

type Host added in v0.3.0

type Host interface {
	// ApplyMention applies a completed mention turn's terminal decision: post the
	// reply, or write the proposed content onto the case and confirm it.
	ApplyMention(ctx context.Context, target Target, decision *Decision) error
	// CreateCase commits a completed create turn's proposal and posts its outcome.
	CreateCase(ctx context.Context, target Target, payload CreatePayload) error
	// AskQuestion posts the planner's question and records it on the session. The
	// turn has ended; the user's answer starts the next one.
	AskQuestion(ctx context.Context, target Target, question QuestionPayload) error
	// ReportFallback tells the user the turn reached no conclusion. reason is the
	// technical cause; the host decides how much of it to show.
	ReportFallback(ctx context.Context, target Target, reason string) error
}

Host is the Slack-facing surface a finished thread-mode turn needs. Each method is called at most once per turn, from the completion handler.

type Mode

type Mode int

Mode discriminates the purpose of a thread-mode turn.

const (
	// ModeMention is a user @-mention in a case thread. The planner may
	// respond, update fields (materialize), or close the case.
	ModeMention Mode = iota
	// ModeMaterialize runs right after a case is auto-created from a
	// monitored-channel post. The planner investigates the message and emits
	// a materialize decision to fill title / description / fields.
	ModeMaterialize
	// ModeCreate runs when a monitored-channel post arrives but NO case exists
	// yet. The planner investigates / asks the user, and only commits a new
	// case (final create decision) once it can satisfy validation.
	ModeCreate
)

type QuestionItem

type QuestionItem struct {
	ID      string
	Text    string
	Type    QuestionItemType
	Options []string
}

QuestionItem is one question within a QuestionPayload.

type QuestionItemType

type QuestionItemType string

QuestionItemType discriminates how the host renders a question's answer control. Mirrors planexec.QuestionItemType values.

const (
	QuestionItemSelect      QuestionItemType = "select"
	QuestionItemMultiSelect QuestionItemType = "multi_select"
	QuestionItemFreeText    QuestionItemType = "free_text"
)

type QuestionPayload

type QuestionPayload struct {
	Reason string
	Items  []QuestionItem
}

QuestionPayload is forwarded to the host when the planner needs human input. The host posts it to the Slack thread; the turn then ends and the user resumes by mentioning the bot again (the conversation history is keyed on Session.ID so the next turn continues seamlessly).

type Result

type Result struct {
	Status Status
	// BusyOwner is the session whose turn holds the thread, set only on
	// StatusBusy.
	BusyOwner *model.Session
}

Result is the outcome of StartTurn.

type Status

type Status int

Status discriminates what StartTurn did.

const (
	// StatusStarted means the turn was spawned; its decision is applied by the
	// run's own completion handler, so the caller has nothing to apply or post.
	StatusStarted Status = iota
	// StatusBusy means another turn holds this thread; BusyOwner names its session.
	StatusBusy
	// StatusIdempotent means the trigger duplicates a turn already started; drop
	// it silently.
	StatusIdempotent
)

type Target added in v0.3.0

type Target struct {
	WorkspaceID string
	// CaseID is 0 for a create turn: the case does not exist yet.
	CaseID    int64
	SessionID string
	// ChannelID / ThreadTS are the run's own thread — the case thread, which the
	// Session is keyed on and where the case's content belongs.
	ChannelID string
	ThreadTS  string
	// UIChannelID / UIThreadTS are the thread the requester is watching. They
	// differ from the pair above only for a case raised by a reaction in another
	// channel; otherwise they hold the same values.
	UIChannelID string
	UIThreadTS  string
	// ProcessID is the run that produced this outcome. A host that records a
	// question needs it so the turn started by the answer can inherit this run's
	// conversation.
	ProcessID string
}

Target locates a finished run: the thread it reports into, and the case and session it belongs to.

It is rebuilt from the Process metadata rather than captured at spawn, because the completion handler runs after the turn — possibly on another instance, where the spawning call's variables no longer exist.

type TurnRequest

type TurnRequest struct {
	Session   *model.Session
	Workspace *model.WorkspaceEntry
	Case      *model.Case

	ChannelID string
	ThreadTS  string
	// UIChannelID / UIThreadTS locate the thread the requester is watching, for the
	// one flow where that is not ChannelID / ThreadTS: a case raised by a reaction
	// lives in the monitored channel while the reactor watches the thread they
	// reacted in. Progress, questions and failure notices go there. Empty means the
	// two are the same thread.
	UIChannelID string
	UIThreadTS  string
	MentionTS   string
	// MentionText is the raw text of the mention that triggered this turn.
	MentionText string
	// MentionUserID / MentionUserName identify its author. The ID is what makes
	// a self-referential request ("assign me") actionable: case__assign takes
	// Slack user IDs and no tool resolves a display name into one. It is also the
	// run's access actor.
	MentionUserID   string
	MentionUserName string

	SystemMessages []ConversationMessage
	DeltaMessages  []ConversationMessage

	// TriggerTS is the Slack TS of the event that started this turn. It is the
	// idempotency key, which is what makes a re-delivered Slack event resolve to
	// the run it already started instead of starting a second one.
	TriggerTS string

	// InheritFrom continues a finished run's conversation in this one. It is how an
	// answered question resumes: the answering turn is a NEW run — its own budget,
	// its own record — but it must see the request, the investigation and the
	// question that produced it. Empty starts a fresh conversation.
	InheritFrom string

	// Mode selects the turn purpose (materialize on creation vs mention).
	Mode Mode

	// CreateInstruction is an optional extra instruction appended to the
	// ModeCreate planner system prompt (under a "# Trigger context" heading).
	// It lets a host inject trigger-specific guidance the generic prompt cannot
	// know — e.g. that the case was raised by a reaction on one message and the
	// surrounding conversation must be read. Empty and ignored for other modes.
	CreateInstruction string
}

TurnRequest collects the inputs resolved by the host before handing control to the threadcase runtime.

Jump to

Keyboard shortcuts

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