summaryrefslogtreecommitdiffstats
path: root/src/session.h
diff options
context:
space:
mode:
Diffstat (limited to 'src/session.h')
-rw-r--r--src/session.h293
1 files changed, 293 insertions, 0 deletions
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 <atomic>
+#include <stdlib.h>
+
+#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<bool> *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<bool> *stop,
+ sess_emit_cb cb, void *user, char **err, int *result);
+
+#endif