From f8423ebea413605b42de23f5dd06c5479d3fa1dd Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 10 Oct 2026 20:55:33 +0000 Subject: initial commit --- src/session.h | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 src/session.h (limited to 'src/session.h') diff --git a/src/session.h b/src/session.h new file mode 100644 index 0000000..ce4dedf --- /dev/null +++ b/src/session.h @@ -0,0 +1,293 @@ +#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 -- cgit v1.3.1