phase 2: interaction + state queries

- GetConfig/SetConfig: per-session config (model, backend, agent, cwd)
- GetContext: JSONL message history
- PeerAdd/PeerRemove/PeerList/PeerSubmit: agent-to-agent communication
- End-to-end tested: CreateSession → Submit → GetChat → KillSession
- 25 C exports total, all wired through D-Bus
This commit is contained in:
Levi Neely 2026-07-05 22:45:30 +02:00
parent 318153493d
commit 8531671556
5 changed files with 389 additions and 0 deletions

View File

@ -62,6 +62,27 @@
<arg name="cost" type="s" direction="out"/>
</method>
<!-- Config -->
<method name="GetConfig">
<arg name="session_id" type="s" direction="in"/>
<arg name="config" type="s" direction="out"/>
<!-- key=value\n pairs -->
</method>
<method name="SetConfig">
<arg name="session_id" type="s" direction="in"/>
<arg name="key" type="s" direction="in"/>
<arg name="value" type="s" direction="in"/>
<arg name="success" type="b" direction="out"/>
</method>
<!-- Context -->
<method name="GetContext">
<arg name="session_id" type="s" direction="in"/>
<arg name="context" type="s" direction="out"/>
<!-- JSONL: one JSON message per line -->
</method>
<!-- Model/backend management -->
<method name="ListBackends">
<arg name="backends" type="as" direction="out"/>
@ -77,6 +98,31 @@
<arg name="agents" type="as" direction="out"/>
</method>
<!-- Peers -->
<method name="PeerAdd">
<arg name="session_id" type="s" direction="in"/>
<arg name="peer_id" type="s" direction="in"/>
<arg name="success" type="b" direction="out"/>
</method>
<method name="PeerRemove">
<arg name="session_id" type="s" direction="in"/>
<arg name="peer_id" type="s" direction="in"/>
<arg name="success" type="b" direction="out"/>
</method>
<method name="PeerList">
<arg name="session_id" type="s" direction="in"/>
<arg name="peers" type="as" direction="out"/>
</method>
<method name="PeerSubmit">
<arg name="session_id" type="s" direction="in"/>
<arg name="peer_id" type="s" direction="in"/>
<arg name="prompt" type="s" direction="in"/>
<arg name="success" type="b" direction="out"/>
</method>
<!-- Signals -->
<signal name="SessionCreated">
<arg name="session_id" type="s"/>

View File

@ -191,6 +191,44 @@ QString SessionManager::GetCost(const QString &session_id)
return cost;
}
// --- Config ---
QString SessionManager::GetConfig(const QString &session_id)
{
QByteArray idUtf8 = session_id.toUtf8();
char *out = nullptr;
int rc = ollie_get_config(const_cast<char*>(idUtf8.constData()), &out);
if (rc != 0 || !out) return QString();
QString config = QString::fromUtf8(out);
ollie_free(out);
return config;
}
bool SessionManager::SetConfig(const QString &session_id, const QString &key, const QString &value)
{
QByteArray idUtf8 = session_id.toUtf8();
QByteArray keyUtf8 = key.toUtf8();
QByteArray valueUtf8 = value.toUtf8();
int rc = ollie_set_config(
const_cast<char*>(idUtf8.constData()),
const_cast<char*>(keyUtf8.constData()),
const_cast<char*>(valueUtf8.constData()));
return rc == 0;
}
// --- Context ---
QString SessionManager::GetContext(const QString &session_id)
{
QByteArray idUtf8 = session_id.toUtf8();
char *out = nullptr;
int rc = ollie_get_context(const_cast<char*>(idUtf8.constData()), &out);
if (rc != 0 || !out) return QString();
QString context = QString::fromUtf8(out);
ollie_free(out);
return context;
}
// --- Backends/models/agents ---
QStringList SessionManager::ListBackends()
@ -223,3 +261,48 @@ QStringList SessionManager::ListAgents()
ollie_free(out);
return raw.split('\n', Qt::SkipEmptyParts);
}
// --- Peers ---
bool SessionManager::PeerAdd(const QString &session_id, const QString &peer_id)
{
QByteArray idUtf8 = session_id.toUtf8();
QByteArray peerUtf8 = peer_id.toUtf8();
int rc = ollie_peer_add(
const_cast<char*>(idUtf8.constData()),
const_cast<char*>(peerUtf8.constData()));
return rc == 0;
}
bool SessionManager::PeerRemove(const QString &session_id, const QString &peer_id)
{
QByteArray idUtf8 = session_id.toUtf8();
QByteArray peerUtf8 = peer_id.toUtf8();
int rc = ollie_peer_remove(
const_cast<char*>(idUtf8.constData()),
const_cast<char*>(peerUtf8.constData()));
return rc == 0;
}
QStringList SessionManager::PeerList(const QString &session_id)
{
QByteArray idUtf8 = session_id.toUtf8();
char *out = nullptr;
ollie_peer_list(const_cast<char*>(idUtf8.constData()), &out);
if (!out) return {};
QString raw = QString::fromUtf8(out);
ollie_free(out);
return raw.split('\n', Qt::SkipEmptyParts);
}
bool SessionManager::PeerSubmit(const QString &session_id, const QString &peer_id, const QString &prompt)
{
QByteArray idUtf8 = session_id.toUtf8();
QByteArray peerUtf8 = peer_id.toUtf8();
QByteArray promptUtf8 = prompt.toUtf8();
int rc = ollie_peer_submit(
const_cast<char*>(idUtf8.constData()),
const_cast<char*>(peerUtf8.constData()),
const_cast<char*>(promptUtf8.constData()));
return rc == 0;
}

View File

@ -36,11 +36,24 @@ public slots:
QString GetUsage(const QString &session_id);
QString GetCost(const QString &session_id);
// Config
QString GetConfig(const QString &session_id);
bool SetConfig(const QString &session_id, const QString &key, const QString &value);
// Context
QString GetContext(const QString &session_id);
// Backends/models/agents
QStringList ListBackends();
QStringList ListModels(const QString &session_id);
QStringList ListAgents();
// Peers
bool PeerAdd(const QString &session_id, const QString &peer_id);
bool PeerRemove(const QString &session_id, const QString &peer_id);
QStringList PeerList(const QString &session_id);
bool PeerSubmit(const QString &session_id, const QString &peer_id, const QString &prompt);
signals:
void SessionCreated(const QString &session_id);
void SessionKilled(const QString &session_id);

View File

@ -19,6 +19,7 @@ static inline void ollie_chat_cb_invoke(ollie_chat_cb cb, const char *sid, int64
import "C"
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
@ -27,6 +28,7 @@ import (
"ollie/pkg/agent"
"ollie/pkg/backend"
"ollie/pkg/config"
"ollie/pkg/env"
"ollie/pkg/paths"
"ollie/pkg/tools"
"ollie/pkg/tools/execute"
@ -50,12 +52,14 @@ type managedSession struct {
agent string
log []byte
logMu sync.Mutex
peers map[string]bool
}
// --- Lifecycle ---
//export ollie_init
func ollie_init(configPath *C.char) C.int {
env.EnsureDefaults()
mu.Lock()
sessions = make(map[string]*managedSession)
mu.Unlock()
@ -381,6 +385,164 @@ func ollie_get_cost(sessionIDC *C.char, out **C.char) C.int {
return 0
}
//export ollie_get_config
func ollie_get_config(sessionIDC *C.char, outBuf **C.char) C.int {
id := C.GoString(sessionIDC)
mu.RLock()
sess, ok := sessions[id]
mu.RUnlock()
if !ok {
return -1
}
// Expose key=value pairs for session config
var result string
result += "model=" + sess.core.ModelName() + "\n"
result += "backend=" + sess.core.BackendName() + "\n"
result += "agent=" + sess.agent + "\n"
result += "cwd=" + sess.core.CWD() + "\n"
result += "state=" + sess.core.State() + "\n"
*outBuf = C.CString(result)
return 0
}
//export ollie_set_config
func ollie_set_config(sessionIDC *C.char, keyC *C.char, valueC *C.char) C.int {
id := C.GoString(sessionIDC)
key := C.GoString(keyC)
value := C.GoString(valueC)
mu.RLock()
sess, ok := sessions[id]
mu.RUnlock()
if !ok {
return -1
}
switch key {
case "model":
// Submit a /model command to change model
sess.core.Submit(sess.ctx, "/model "+value)
case "cwd":
if err := sess.core.SetCWD(value); err != nil {
return -1
}
default:
return -1 // unknown key
}
return 0
}
//export ollie_get_context
func ollie_get_context(sessionIDC *C.char, outBuf **C.char) C.int {
id := C.GoString(sessionIDC)
mu.RLock()
sess, ok := sessions[id]
mu.RUnlock()
if !ok {
return -1
}
// Return context as JSONL (one JSON message per line)
msgs := sess.core.Context()
var result string
for _, msg := range msgs {
data, err := json.Marshal(msg)
if err != nil {
continue
}
result += string(data) + "\n"
}
*outBuf = C.CString(result)
return 0
}
// --- Peers ---
//export ollie_peer_add
func ollie_peer_add(sessionIDC *C.char, peerIDC *C.char) C.int {
id := C.GoString(sessionIDC)
peerID := C.GoString(peerIDC)
mu.RLock()
_, ok := sessions[id]
_, peerOk := sessions[peerID]
mu.RUnlock()
if !ok || !peerOk {
return -1
}
mu.Lock()
sess := sessions[id]
if sess.peers == nil {
sess.peers = make(map[string]bool)
}
sess.peers[peerID] = true
mu.Unlock()
return 0
}
//export ollie_peer_remove
func ollie_peer_remove(sessionIDC *C.char, peerIDC *C.char) C.int {
id := C.GoString(sessionIDC)
peerID := C.GoString(peerIDC)
mu.Lock()
sess, ok := sessions[id]
if !ok {
mu.Unlock()
return -1
}
delete(sess.peers, peerID)
mu.Unlock()
return 0
}
//export ollie_peer_list
func ollie_peer_list(sessionIDC *C.char, outBuf **C.char) C.int {
id := C.GoString(sessionIDC)
mu.RLock()
sess, ok := sessions[id]
mu.RUnlock()
if !ok {
return -1
}
var result string
for peerID := range sess.peers {
result += peerID + "\n"
}
*outBuf = C.CString(result)
return C.int(len(sess.peers))
}
//export ollie_peer_submit
func ollie_peer_submit(sessionIDC *C.char, peerIDC *C.char, promptC *C.char) C.int {
id := C.GoString(sessionIDC)
peerID := C.GoString(peerIDC)
prompt := C.GoString(promptC)
mu.RLock()
sess, ok := sessions[id]
peer, peerOk := sessions[peerID]
mu.RUnlock()
if !ok || !peerOk {
return -1
}
// Verify peer is registered
if !sess.peers[peerID] {
return -1
}
// Submit to the peer session
go peer.core.Submit(peer.ctx, prompt)
return 0
}
// --- Backends/models/agents ---
//export ollie_list_backends

85
test-e2e.sh Executable file
View File

@ -0,0 +1,85 @@
#!/bin/sh
# End-to-end test for ollie-kde Phase 2
# Run this while the daemon is running: make run
set -e
SERVICE="org.ollie.SessionManager"
PATH_OBJ="/org/ollie/SessionManager"
IFACE="org.ollie.SessionManager"
call() {
dbus-send --session --dest=$SERVICE --type=method_call --print-reply \
$PATH_OBJ "$IFACE.$1" "${@:2}"
}
echo "=== ListBackends ==="
call ListBackends
echo ""
echo "=== ListAgents ==="
call ListAgents
echo ""
echo "=== CreateSession ==="
RESULT=$(dbus-send --session --dest=$SERVICE --type=method_call --print-reply \
$PATH_OBJ $IFACE.CreateSession \
string:"$HOME" string:"" string:"" string:"default")
echo "$RESULT"
SESSION_ID=$(echo "$RESULT" | grep 'string "' | head -1 | sed 's/.*string "//;s/"//')
echo "Session ID: $SESSION_ID"
if [ -z "$SESSION_ID" ]; then
echo "FAIL: no session created"
exit 1
fi
echo ""
echo "=== GetState ==="
call GetState string:"$SESSION_ID"
echo ""
echo "=== GetConfig ==="
call GetConfig string:"$SESSION_ID"
echo ""
echo "=== Submit prompt ==="
call Submit string:"$SESSION_ID" string:"Say hello in exactly 5 words."
echo ""
echo "=== Waiting 5s for response... ==="
sleep 5
echo ""
echo "=== GetState (should be idle) ==="
call GetState string:"$SESSION_ID"
echo ""
echo "=== GetChat (offset 0) ==="
dbus-send --session --dest=$SERVICE --type=method_call --print-reply \
$PATH_OBJ $IFACE.GetChat \
string:"$SESSION_ID" int64:0
echo ""
echo "=== GetUsage ==="
call GetUsage string:"$SESSION_ID"
echo ""
echo "=== GetCost ==="
call GetCost string:"$SESSION_ID"
echo ""
echo "=== GetContext (first 200 chars) ==="
CONTEXT=$(dbus-send --session --dest=$SERVICE --type=method_call --print-reply \
$PATH_OBJ $IFACE.GetContext string:"$SESSION_ID" 2>&1)
echo "$CONTEXT" | head -5
echo ""
echo "=== KillSession ==="
call KillSession string:"$SESSION_ID"
echo ""
echo "=== ListSessions (should be empty) ==="
call ListSessions
echo ""
echo "=== DONE ==="