#ifndef LLSH_SESSION_H #define LLSH_SESSION_H #include #include #include "config.h" #include "message.h" #include "compact.h" #include "goal.h" /* ------------------------------------------------------------------ */ /* Model abstraction (mirrors the rig `Agent` a session drives). */ /* A model is an opaque object with a small vtable: a single-shot */ /* `complete` (summarizer / goal evaluator) and a `stream` that drains */ /* one turn, emitting events as they arrive. */ /* ------------------------------------------------------------------ */ /* One unit the stream can emit, in arrival order. */ enum { SESS_EMIT_TEXT = 0, SESS_EMIT_REASONING, SESS_EMIT_VERBATIM, SESS_EMIT_STATUS, SESS_EMIT_RESULT, }; typedef struct sess_emit { int kind; /* one of SESS_EMIT_* */ char *text; /* TEXT/REASONING/VERBATIM/RESULT: body */ bool from_shell; /* RESULT: came from a shell-style tool */ unsigned long input_tokens; /* STATUS: the new context size */ } sess_emit; void sess_emit_free(sess_emit *e); /* Callback invoked for each emit. Return 0 to continue, non-zero to abort * (a user stop). */ typedef int (*sess_emit_cb)(const sess_emit *e, void *user); /* Error reported by a model call. `message` is owned by the receiver and * must be freed; the other fields are plain values. */ typedef struct sess_error { char *message; int http_status; /* 0 = no HTTP status */ int transport; /* 1 = bare transport error (no status) */ unsigned long retry_after_secs; /* 0 = none */ } sess_error; void sess_error_clear(sess_error *e); /* Terminal result of a model `stream` call. */ enum { SESS_STREAM_OK = 0, SESS_STREAM_RETRYABLE, /* transient, safe to retry (no output committed) */ SESS_STREAM_FAILED, /* non-retryable failure */ SESS_STREAM_STOPPED, /* the user stopped the turn */ }; typedef struct llsh_model llsh_model; struct llsh_model { /* Single-shot completion (summarizer / goal evaluator). Returns the * model's text (caller frees) or NULL on error (`err` filled). */ char *(*complete)(const llsh_model *m, const char *prompt, sess_error *err); /* Stream one turn: send history + prompt, call `cb` for each emit. * `stop` is polled between emits; if set (or `cb` returns non-zero) the * stream stops and returns SESS_STREAM_STOPPED. * On SESS_STREAM_OK / SESS_STREAM_STOPPED: * - *assistant_text (caller frees; may be empty) is the turn's prose; * - *input_tokens (last completion call's input tokens, or 0) is set; * - if the model produced structured history, *new_messages is an * allocated array of owned `message` values and *n_new is its length; * the caller takes ownership (release with `delete[]`). * When no structured history is produced, leave *new_messages NULL and * *n_new 0; the caller falls back to `assistant_text`. * On SESS_STREAM_RETRYABLE / SESS_STREAM_FAILED fills *err. */ int (*stream)(const llsh_model *m, const message *history, size_t nhistory, const char *prompt, sess_emit_cb cb, void *user, std::atomic *stop, char **assistant_text, unsigned long *input_tokens, message **new_messages, size_t *n_new, sess_error *err); /* Optional destructor. When set, `llsh_model_free` calls it; otherwise * it falls back to plain `free`. */ void (*destroy)(llsh_model *m); }; static inline void llsh_model_free(llsh_model *m) { if (!m) return; if (m->destroy) m->destroy(m); else free(m); } /* ------------------------------------------------------------------ */ /* Pure helpers (no session state; directly unit-testable). */ /* ------------------------------------------------------------------ */ /* HTTP statuses treated as transient (worth retrying). */ bool sess_is_transient_status(int status); /* Exponential backoff for the `attempt`-th retry (1-indexed), in whole * seconds: 1, 2, 4, 8, 16, then capped at 30. */ unsigned long sess_backoff_secs(unsigned long attempt); /* Render a duration (whole seconds, at least 1) for a retry notice. */ unsigned long sess_human_secs(unsigned long secs); /* Classification of a retryable provider failure. */ typedef struct sess_retry { char *reason; /* owned */ unsigned long retry_after_secs; /* 0 = none */ bool has_retry_after; } sess_retry; void sess_retry_free(sess_retry *r); /* Classify a model error as retryable. Returns a freshly allocated * sess_retry, or NULL if the error is permanent. Caller frees. */ sess_retry *sess_classify_retry(const sess_error *e); /* True when a model error means the request exceeded the provider's context * window (a 400-class error mentioning context length, or a 413). The turn * can recover by compacting history and retrying once. */ bool sess_error_is_context_overflow(const sess_error *e); /* Map one streamed assistant content item to an emit. * `kind`: "text" | "reasoning" | "tool_call". * `text`: the body (text/reasoning) or, for a tool call, the tool name. * `args`: for a tool call, the args JSON; else NULL. * Returns 0 and fills *out (caller frees the fields), or 1 for no emit. */ int sess_assistant_emit(const char *kind, const char *text, const char *args, sess_emit *out); /* Render a tool's result for display: the full result body, normalized to * end with exactly one newline. Returns 0 and fills *out (caller frees the * fields), or 1 for no emit (NULL / empty result). */ int sess_tool_result_emit(const char *tool, const char *result, sess_emit *out); /* The models.json `default` preset name active at startup, or NULL. * Caller frees. */ char *session_active_preset_from(const dotagents *dot); /* The base system preamble: `.agents` system-prompt.md (a full override) or * the configured preamble, with the project sections appended. Caller frees. */ char *sess_base_preamble_for(const config *cfg, const dotagents *dot); /* The full preamble for a profile: the sub-agent's agent.md body plus the * project sections, or the base preamble when the profile is inactive or its * sub-agent is missing. Caller frees. `profile` may be NULL. */ char *sess_preamble_for_profile(const dotagents *dot, const config *cfg, const char *profile); /* Apply model/connection overrides onto `cfg`. In strict mode (a preset) an * unparseable provider is a hard error; otherwise it is skipped. Returns 0 on * success, -1 on error (*err set). */ int sess_apply_overrides(config *cfg, const char *model, const char *provider, const char *base_url, const char *api_key, bool strict, char **err); /* The effective config for a profile: a deep copy of `cfg` with the * profile's config.json overrides applied. Caller frees with config_free. * `profile` may be NULL (returns a plain clone). */ config sess_effective_for_profile(const config *cfg, const dotagents *dot, const char *profile); /* ------------------------------------------------------------------ */ /* Session */ /* ------------------------------------------------------------------ */ typedef struct session session; /* Turn result. */ enum { SESS_TURN_COMPLETED = 0, SESS_TURN_STOPPED = 1 }; /* Adopt `cfg`, `dot`, and `model` (the caller no longer owns them). */ session *session_new_with_model(llsh_model *model, config cfg, dotagents dot); void session_free(session *s); /* Inject a tool-less summarizer / goal evaluator / rubric judge (adopted). * Returns s. */ session *session_with_summarizer(session *s, llsh_model *summarizer); session *session_with_goal_evaluator(session *s, llsh_model *evaluator); session *session_with_rubric(session *s, llsh_model *rubric); /* -- accessors (caller frees returned strings) -- */ char *session_model(session *s); const config *session_config(const session *s); char *session_active_preset(session *s); char *session_profile_name(session *s); const dotagents *session_dot(const session *s); /* borrowed; do not free */ /* Allowed built-in tools for the active profile (borrowed; do not free). * An empty list means "all built-in tools allowed". */ const strlist *session_allowed_tools(session *s); unsigned long session_context_tokens(session *s); unsigned long session_context_limit(session *s); const goal_report *session_goal_report(session *s); void session_set_goal_report(session *s, goal_report *r); /* adopts r */ char *session_last_assistant_text(session *s); /* Both return borrowed state, valid until the next set_model / set_profile / * refresh_dot / apply_preset call. */ const char *session_current_preamble(session *s); const config *session_active_config(session *s); int session_refresh_context_limit(session *s); /* 1 if a provider-reported limit was adopted, 0 if none was learned */ /* -- history -- */ size_t session_history_len(session *s); const message *session_history_at(session *s, size_t i); void session_push_history(session *s, message m); /* copies m */ unsigned long session_last_context_tokens(session *s); void session_set_last_context_tokens(session *s, unsigned long t); void session_clear_history(session *s); /* Remove the most recent user turn. Returns -1 when none, else the number of * messages removed. */ int session_undo_last_turn(session *s); size_t session_message_count(session *s); char *session_transcript_markdown(session *s); /* -- compaction -- */ /* Run the two-tier compaction pass if the context is large enough (or always, * when `force`). Returns 0 = no pass (report untouched), 1 = pass ran * (report filled), -1 = error (*err set). */ int session_compact_if_needed(session *s, bool force, compact_report *report, char **err); void session_print_compact(const compact_report *report); /* -- rubric compaction (SelfCompact, arXiv:2606.23525) -- */ /* Record one completed tool round. The provider calls this after each tool * round; it feeds the rubric probe gates. */ void session_note_tool_round(session *s); /* Model-invoked compaction (the `compact` tool): run the pass unconditionally. * Returns 0 = no pass (history too short), 1 = pass ran (report filled), * -1 = error (*err set). */ int session_force_compact(session *s, compact_report *report, char **err); /* End-of-turn rubric probe: when the gates are open, ask the summarizer to * judge the trajectory and compact only if the rubric fires. Returns * 0 = no compaction, 1 = compacted, -1 = error (*err set). */ int session_rubric_probe(session *s, char **err); /* -- goal -- */ int session_evaluate_goal(session *s, const goal_spec *spec, const char *last_reply, goal_verdict *out, char **err); /* -- model / profile -- */ int session_set_model(session *s, const char *name, char **err); int session_set_profile(session *s, const char *id, char **err); int session_refresh_dot(session *s, const char *workspace, const char *global, char **err); /* -- debug dump -- */ /* Set (or clear with NULL) the file the whole conversation history is * written to when `--debug` is active. `path` is adopted. */ void session_set_debug_path(session *s, char *path); /* Write the current history to the debug path (no-op when unset). * Returns 0 on success / no-op, -1 on error (*err set, caller frees). */ int session_debug_dump(session *s, char **err); /* -- save / load / resume -- */ typedef struct session_snapshot { unsigned long version; char *cwd; unsigned long long saved_at; config cfg; /* owned (name avoids the C++ type/member clash) */ history hist; /* owned */ char *active_preset; /* may be NULL */ char *profile; /* may be NULL */ unsigned long last_context_tokens; } session_snapshot; void session_snapshot_free(session_snapshot *snap); session_snapshot *session_snapshot_of(session *s); int session_save(session *s, const char *path, char **err); session_snapshot *session_load(const char *path, char **err); /* Adopt `snap`, `dot`, and `model`; free the snapshot. */ session *session_resume(session_snapshot *snap, dotagents dot, llsh_model *model, char **err); /* Run one user turn. `stop` may be set by the caller/cb to abort; if already * set the prompt is kept in context and the turn is Stopped. `err` and * `result` are optional. Returns 0 on a completed/stopped turn, -1 on error. */ int session_run_turn(session *s, const char *prompt, std::atomic *stop, sess_emit_cb cb, void *user, char **err, int *result); #endif