/* AgentCore: the Qt wrapper around one cst_agent_t. * * The engine runs turns on a worker thread; every cst_agent_hooks_t callback * is turned into a Qt signal, and the two hooks that need an answer * (on_permission, on_ask_user) block the worker until the GUI replies. No * HTTP, no socket: the GUI owns the engine in its own process. */ #ifndef CST_APP_AGENTCORE_H #define CST_APP_AGENTCORE_H #include #include #include #include #include #include #include #include "cst_agent.h" #include "cst_tools.h" class QThread; class AgentCore : public QObject { Q_OBJECT Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(QString workspace READ workspace NOTIFY workspaceChanged) Q_PROPERTY(QString mode READ mode NOTIFY modeChanged) Q_PROPERTY(QString provider READ provider NOTIFY configChanged) Q_PROPERTY(QString model READ model NOTIFY configChanged) Q_PROPERTY(int approval READ approval WRITE setApproval NOTIFY approvalChanged) Q_PROPERTY(bool planMode READ planMode WRITE setPlanMode NOTIFY planModeChanged) Q_PROPERTY(QString todo READ todo NOTIFY todoChanged) Q_PROPERTY(QString sessionId READ sessionId NOTIFY sessionChanged) public: explicit AgentCore(QObject *parent = nullptr); ~AgentCore() override; /* Start the engine on a workspace. provider/model/apiKey may be empty to * fall back to the saved credentials and the provider's default model. */ bool open(const QString &workspace, const QString &provider, const QString &model, const QString &apiKey, QString *err); void close(); bool running() const { return m_running; } QString workspace() const { return m_workspace; } QString mode() const { return QString::fromUtf8(m_cfg.mode); } QString provider() const { return QString::fromUtf8(m_cfg.provider); } QString model() const { return QString::fromUtf8(m_cfg.model); } int approval() const { return m_cfg.approval; } bool planMode() const { return m_cfg.plan_mode != 0; } QString todo() const { return m_todo; } QString sessionId() const; /* Markdown -> HTML with the engine's own renderer (cst_web_md_html), so a * message looks the same here as it does in the WebUI. */ Q_INVOKABLE QString renderMarkdown(const QString &text) const; /* --- sessions (the workspace's .agent/sessions store) ------------------- */ /* Stored sessions, newest first; one map each: id, title (a rename, else * empty), firstUser, messages, mtime (ms since the epoch), current. */ Q_INVOKABLE QVariantList sessions() const; /* The open session as transcript rows, in exactly the shape the live * signals build (kind, role, text, name, state, detail, output, exitCode, * complete), so a reopened session renders like the turn that wrote it. */ Q_INVOKABLE QVariantList history() const; /* None of these run during a turn; all return false when one does. */ Q_INVOKABLE bool newSession(); Q_INVOKABLE bool switchSession(const QString &id); Q_INVOKABLE bool deleteSession(const QString &id); Q_INVOKABLE bool renameSession(const QString &id, const QString &title); /* --- provider, model, key (see cst_provider_switch.h) ------------------- */ /* The presets: name, base, model (remembered, else the preset's), hasKey * and keyFrom ("saved", else the environment variable), keyEnv, source and * current, custom (the user's own provider, kept in the engine's user * model file). Never a key: the engine keeps those out of the UI's reach. */ Q_INVOKABLE QVariantList providers() const; /* A provider the engine's presets do not know: it goes into the user model * file (/cst-agent/models.json), which is where the registry keeps * them, and the registry is reloaded, so the next providers() call lists * it. `modelsCsv` is a comma-separated model list (may be empty). * "" when it worked, else the reason. */ Q_INVOKABLE QString addProvider(const QString &name, const QString &base, const QString &keyEnv, const QString &defaultModel, const QString &modelsCsv); /* Drop a provider that came from that file (1 = it was there, 0 = it was * not, -1 = with a reason). A preset of the same name stays. */ Q_INVOKABLE int removeProvider(const QString &name); /* The generation parameters remembered for a provider (0 = not set, the * field is not sent at all). */ Q_INVOKABLE QVariantMap genParams(const QString &provider) const; /* Remember temperature / top-p / max tokens (0 = unset) for a provider and * apply them to the engine when that provider is the live one. * "" when it worked, else the reason. */ Q_INVOKABLE QString setGenParams(const QString &provider, double temperature, double topP, int maxTokens); /* Model candidates for a provider: its own list, what was fetched into the * cache, and the remembered pick. No network. */ Q_INVOKABLE QStringList modelChoices(const QString &provider) const; /* "1M ยท vision" for the model picker, "" when nothing is known. */ Q_INVOKABLE QString modelInfo(const QString &model) const; /* Switch provider, model and key in one call. The engine's own config moves * (its session does not), the choice is remembered, and an empty return * means it worked; anything else is a reason to show. A key is verified * against the provider's /models when one is given. */ Q_INVOKABLE QString useProvider(const QString &provider, const QString &model, const QString &key); /* Tokens of this session and of the last turn, plus the cost when priced. */ Q_INVOKABLE QVariantMap usage() const; /* --- attachments (see cst_attach.h) ------------------------------------- */ Q_PROPERTY(QStringList pending READ pendingAttachments NOTIFY pendingChanged) /* What the next prompt will carry: " (, 12 KB)" per line. */ QStringList pendingAttachments() const; /* Copy a file next to the session and give it to the next prompt. "" when * it worked, else why not. */ Q_INVOKABLE QString attachFile(const QString &path); /* The same for an image on the clipboard (Ctrl+V). "" when it worked; a * reason -- "no image on the clipboard" -- when the editor should have the * keystroke instead. */ Q_INVOKABLE QString attachClipboard(); /* Whether Ctrl+V should become an attachment at all (an image is on the * clipboard) or stay with the editor (text). */ Q_INVOKABLE bool clipboardHasImage() const; Q_INVOKABLE void clearPending(); /* 1 the model is shown images, 0 it is not (they go as notes), -1 unknown. */ Q_INVOKABLE int seesImages() const; /* The engine, for the parts that need it (tool registration). */ cst_agent_t *agent() const { return m_agent; } const cst_agent_config_t *config() const { return &m_cfg; } /* Ask a question; emits the streaming signals, then turnFinished(). */ Q_INVOKABLE void send(const QString &prompt); Q_INVOKABLE void cancel(); /* Consent: answers map to CST_PERM_*. */ Q_INVOKABLE void answerPermission(int choice); Q_INVOKABLE void answerQuestion(const QString &answer); Q_INVOKABLE void setMode(const QString &mode); /* coding | general */ Q_INVOKABLE void setApproval(int approval); /* cst_approval_t */ Q_INVOKABLE void setPlanMode(bool on); /* Tools the front end implements itself. The engine copies the definition * (strings included), so a stack value is fine; ud must outlive it. * owner is what unregisterOwner() later matches. */ bool registerTool(const cst_tool_def_t *def); void unregisterOwner(const QString &owner); /* Re-probe platform tools after a service appeared or went away. */ void reloadToolMeta() { cst_tool_meta_reload(); } /* Answer every consent question with "once" (the C smoke test, headless * runs, a --yes flag). */ void setAutoApprove(bool on) { m_autoApprove = on; } signals: void configChanged(); void approvalChanged(); void planModeChanged(); void sessionChanged(); /* another session is open now */ void sessionListChanged(); /* the store gained, lost or renamed one */ void pendingChanged(); /* attachments for the next prompt changed */ void statusChanged(const QString &text); void assistantDelta(const QString &text); void thinkDelta(const QString &text); /* A call is identified by its id: the same tool can run twice in one turn, * and only the id tells the two cards apart. Text protocols carry ids too * (cst_tool_call_from_native, the ctl parser), so this is never empty. */ void toolEvent(const QString &phase, const QString &id, const QString &name, const QString &detail); void toolResult(const QString &id, const QString &name, int exitCode, const QString &output); void todoChanged(const QString &text); void permissionRequested(const QString &tool, const QString &detail, const QString &preview); void questionAsked(const QString &question, const QString &options); void turnFinished(int rc, const QString &err); void runningChanged(); void workspaceChanged(); void modeChanged(); private: friend class Turn; void finishTurn(int rc, const QString &err); void setRunning(bool on); /* Take the engine's config over m_cfg again (a switch changed it) and say * so. */ void syncConfig(); /* Re-read what a session switch changed (id, todo) and say so. */ void afterSessionChange(); /* Hook trampolines (C, run on the worker thread). */ static void hookStatus(const char *msg, void *ud); static void hookAssistant(const char *text, size_t len, void *ud); static void hookThink(const char *text, size_t len, void *ud); static void hookToolEvent(const char *phase, const ctl_tool_call_t *call, const char *detail, void *ud); static void hookToolResult(const ctl_tool_call_t *call, const char *out, size_t len, int exit_code, void *ud); static void hookTodo(const char *text, void *ud); static int hookPermission(const char *tool, const char *detail, const char *preview, void *ud); static int hookAskUser(const char *question, const char *options, char *ans, size_t ans_sz, void *ud); /* Post a lambda onto the GUI thread. */ template void post(F f) { QMetaObject::invokeMethod(this, f, Qt::QueuedConnection); } cst_agent_t *m_agent = nullptr; cst_agent_config_t m_cfg; cst_agent_hooks_t m_hooks; QString m_workspace; QThread *m_turn = nullptr; bool m_running = false; bool m_closing = false; /* consent handshake between the worker and the GUI thread */ QMutex m_askMu; QWaitCondition m_askCv; int m_permAnswer = CST_PERM_DENY; bool m_permPending = false; QString m_question; QString m_todo; /* Names of the files this window attached, in the engine's pending order: * the engine's own description says what it is, not where it came from. */ QStringList m_attachNames; QByteArray m_answer; bool m_questionPending = false; bool m_autoApprove = false; }; #endif /* CST_APP_AGENTCORE_H */