summaryrefslogtreecommitdiffstats
path: root/src/session.h
blob: ce4dedff67e9ccee2efdfddbee692ccb81f6c94b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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