612 lines
12 KiB
Go
612 lines
12 KiB
Go
package main
|
|
|
|
/*
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
|
|
// Callback typedefs — set by the C++ daemon to receive push events.
|
|
typedef void (*ollie_state_cb)(const char *session_id, const char *state);
|
|
typedef void (*ollie_chat_cb)(const char *session_id, int64_t offset, const char *text, int len);
|
|
|
|
// Trampolines: cgo cannot call function pointers directly from Go.
|
|
static inline void ollie_state_cb_invoke(ollie_state_cb cb, const char *sid, const char *state) {
|
|
if (cb) cb(sid, state);
|
|
}
|
|
static inline void ollie_chat_cb_invoke(ollie_chat_cb cb, const char *sid, int64_t offset, const char *text, int len) {
|
|
if (cb) cb(sid, offset, text, len);
|
|
}
|
|
*/
|
|
import "C"
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"ollie/pkg/agent"
|
|
"ollie/pkg/backend"
|
|
"ollie/pkg/config"
|
|
"ollie/pkg/env"
|
|
"ollie/pkg/paths"
|
|
"ollie/pkg/tools"
|
|
"ollie/pkg/tools/execute"
|
|
)
|
|
|
|
// --- Global state ---
|
|
|
|
var (
|
|
mu sync.RWMutex
|
|
sessions map[string]*managedSession
|
|
stateCB C.ollie_state_cb
|
|
chatCB C.ollie_chat_cb
|
|
)
|
|
|
|
// managedSession wraps a Core with its context and metadata.
|
|
type managedSession struct {
|
|
core agent.Core
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
id string
|
|
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()
|
|
return 0
|
|
}
|
|
|
|
//export ollie_shutdown
|
|
func ollie_shutdown() {
|
|
mu.Lock()
|
|
for id, sess := range sessions {
|
|
sess.core.Interrupt(agent.ErrInterrupted)
|
|
sess.core.Close()
|
|
sess.cancel()
|
|
delete(sessions, id)
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
|
|
// --- Sessions ---
|
|
|
|
//export ollie_create_session
|
|
func ollie_create_session(cwdC *C.char, backendC *C.char, modelC *C.char, agentC *C.char, outID **C.char) C.int {
|
|
cwd := C.GoString(cwdC)
|
|
backendName := C.GoString(backendC)
|
|
modelName := C.GoString(modelC)
|
|
agentName := C.GoString(agentC)
|
|
|
|
if cwd == "" {
|
|
cwd, _ = os.Getwd()
|
|
}
|
|
cwd = paths.ExpandHome(os.ExpandEnv(cwd))
|
|
|
|
if agentName == "" {
|
|
agentName = "default"
|
|
if v := os.Getenv("OLLIE_DEFAULT_AGENT"); v != "" {
|
|
agentName = v
|
|
}
|
|
}
|
|
|
|
sessID := agent.NewSessionID()
|
|
|
|
// Load agent config
|
|
agentsDir := paths.CfgDir() + "/agents"
|
|
var cfg *config.Config
|
|
cfgPath := agent.AgentConfigPath(agentsDir, agentName)
|
|
if f, err := os.Open(cfgPath); err == nil {
|
|
cfg, _ = config.Load(f)
|
|
f.Close()
|
|
}
|
|
|
|
// Resolve backend
|
|
if backendName == "" && cfg != nil && cfg.Backend != "" {
|
|
backendName = cfg.Backend
|
|
}
|
|
be, err := backend.NewWithName(backendName)
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
|
|
// Resolve model
|
|
if modelName == "" && cfg != nil && cfg.Model != "" {
|
|
modelName = cfg.Model
|
|
}
|
|
if modelName == "" {
|
|
modelName = os.Getenv("OLLIE_MODEL")
|
|
}
|
|
if modelName != "" {
|
|
be.SetModel(modelName)
|
|
}
|
|
|
|
// Sessions dir
|
|
sessionsDir := paths.DataDir() + "/sessions"
|
|
os.MkdirAll(sessionsDir, 0700)
|
|
|
|
// Build dispatcher + runtime
|
|
newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{
|
|
"execute": execute.Decl(cwd),
|
|
})
|
|
rt := agent.BuildRuntime(cfg, newDisp(), cwd, []string{"OLLIE_SESSION_ID=" + sessID})
|
|
|
|
// Create the agent core
|
|
core := agent.NewAgentCore(agent.AgentCoreConfig{
|
|
Backend: be,
|
|
AgentName: agentName,
|
|
AgentsDir: agentsDir,
|
|
SessionsDir: sessionsDir,
|
|
SessionID: sessID,
|
|
CWD: cwd,
|
|
Runtime: rt,
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
sess := &managedSession{
|
|
core: core,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
id: sessID,
|
|
agent: agentName,
|
|
}
|
|
|
|
// Subscribe to events for the chat log + callback dispatch
|
|
core.Bus().Subscribe("event", func(ev agent.Event) {
|
|
var text string
|
|
switch ev.Role {
|
|
case "assistant":
|
|
text = ev.Content
|
|
case "info":
|
|
text = "[info] " + ev.Content
|
|
case "tool":
|
|
text = "[tool:" + ev.Name + "] " + ev.Content
|
|
default:
|
|
text = "[" + ev.Role + "] " + ev.Content
|
|
}
|
|
if text == "" {
|
|
return
|
|
}
|
|
|
|
sess.logMu.Lock()
|
|
offset := int64(len(sess.log))
|
|
sess.log = append(sess.log, []byte(text)...)
|
|
sess.logMu.Unlock()
|
|
|
|
if chatCB != nil {
|
|
cID := C.CString(sessID)
|
|
cText := C.CString(text)
|
|
C.ollie_chat_cb_invoke(chatCB, cID, C.int64_t(offset), cText, C.int(len(text)))
|
|
C.free(unsafe.Pointer(cID))
|
|
C.free(unsafe.Pointer(cText))
|
|
}
|
|
})
|
|
|
|
// Watch state changes in a goroutine
|
|
go func() {
|
|
current := "idle"
|
|
for {
|
|
next, ok := core.WaitChange(ctx, agent.WatchState, current)
|
|
if !ok {
|
|
return
|
|
}
|
|
current = next
|
|
if stateCB != nil {
|
|
cID := C.CString(sessID)
|
|
cState := C.CString(current)
|
|
C.ollie_state_cb_invoke(stateCB, cID, cState)
|
|
C.free(unsafe.Pointer(cID))
|
|
C.free(unsafe.Pointer(cState))
|
|
}
|
|
}
|
|
}()
|
|
|
|
mu.Lock()
|
|
sessions[sessID] = sess
|
|
mu.Unlock()
|
|
|
|
*outID = C.CString(sessID)
|
|
return 0
|
|
}
|
|
|
|
//export ollie_kill_session
|
|
func ollie_kill_session(sessionIDC *C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
mu.Lock()
|
|
sess, ok := sessions[id]
|
|
if !ok {
|
|
mu.Unlock()
|
|
return -1
|
|
}
|
|
delete(sessions, id)
|
|
mu.Unlock()
|
|
|
|
sess.core.Interrupt(agent.ErrInterrupted)
|
|
sess.core.Close()
|
|
sess.cancel()
|
|
return 0
|
|
}
|
|
|
|
//export ollie_rename_session
|
|
func ollie_rename_session(sessionIDC *C.char, newNameC *C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
newName := C.GoString(newNameC)
|
|
|
|
mu.Lock()
|
|
sess, ok := sessions[id]
|
|
if !ok {
|
|
mu.Unlock()
|
|
return -1
|
|
}
|
|
delete(sessions, id)
|
|
sess.id = newName
|
|
sessions[newName] = sess
|
|
mu.Unlock()
|
|
|
|
if err := sess.core.SetSessionID(newName); err != nil {
|
|
mu.Lock()
|
|
delete(sessions, newName)
|
|
sess.id = id
|
|
sessions[id] = sess
|
|
mu.Unlock()
|
|
return -1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
//export ollie_list_sessions
|
|
func ollie_list_sessions(outBuf **C.char) C.int {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
var result string
|
|
for id, sess := range sessions {
|
|
result += fmt.Sprintf("%s\t%s\t%s\t%s\n",
|
|
id, sess.core.State(), sess.core.ModelName(), sess.agent)
|
|
}
|
|
*outBuf = C.CString(result)
|
|
return C.int(len(sessions))
|
|
}
|
|
|
|
// --- Interaction ---
|
|
|
|
//export ollie_submit
|
|
func ollie_submit(sessionIDC *C.char, promptC *C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
prompt := C.GoString(promptC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
go sess.core.Submit(sess.ctx, prompt)
|
|
return 0
|
|
}
|
|
|
|
//export ollie_interrupt
|
|
func ollie_interrupt(sessionIDC *C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
sess.core.Interrupt(agent.ErrInterrupted)
|
|
return 0
|
|
}
|
|
|
|
// --- State queries ---
|
|
|
|
//export ollie_get_state
|
|
func ollie_get_state(sessionIDC *C.char, outState **C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
*outState = C.CString(sess.core.State())
|
|
return 0
|
|
}
|
|
|
|
//export ollie_get_chat
|
|
func ollie_get_chat(sessionIDC *C.char, offset C.int64_t, outText **C.char, outNewOffset *C.int64_t) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
sess.logMu.Lock()
|
|
off := int64(offset)
|
|
if off < 0 {
|
|
off = 0
|
|
}
|
|
if off > int64(len(sess.log)) {
|
|
off = int64(len(sess.log))
|
|
}
|
|
text := string(sess.log[off:])
|
|
newOff := int64(len(sess.log))
|
|
sess.logMu.Unlock()
|
|
|
|
*outText = C.CString(text)
|
|
*outNewOffset = C.int64_t(newOff)
|
|
return 0
|
|
}
|
|
|
|
//export ollie_get_usage
|
|
func ollie_get_usage(sessionIDC *C.char, out **C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
*out = C.CString(sess.core.Usage())
|
|
return 0
|
|
}
|
|
|
|
//export ollie_get_cost
|
|
func ollie_get_cost(sessionIDC *C.char, out **C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
*out = C.CString(sess.core.Cost())
|
|
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
|
|
func ollie_list_backends(out **C.char) C.int {
|
|
backends := "ollama\nopenai\nopenrouter\nanthropic\ncopilot\nkiro\ngemini\n"
|
|
*out = C.CString(backends)
|
|
return 7
|
|
}
|
|
|
|
//export ollie_list_models
|
|
func ollie_list_models(sessionIDC *C.char, out **C.char) C.int {
|
|
id := C.GoString(sessionIDC)
|
|
|
|
mu.RLock()
|
|
sess, ok := sessions[id]
|
|
mu.RUnlock()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
|
|
*out = C.CString(sess.core.ListModels())
|
|
return 0
|
|
}
|
|
|
|
//export ollie_list_agents
|
|
func ollie_list_agents(out **C.char) C.int {
|
|
var result string
|
|
var count int
|
|
for _, dir := range agent.AgentsDirs() {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if len(name) > 5 && name[len(name)-5:] == ".json" {
|
|
result += name[:len(name)-5] + "\n"
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
*out = C.CString(result)
|
|
return C.int(count)
|
|
}
|
|
|
|
// --- Callbacks ---
|
|
|
|
//export ollie_on_state_change
|
|
func ollie_on_state_change(cb C.ollie_state_cb) {
|
|
stateCB = cb
|
|
}
|
|
|
|
//export ollie_on_chat_update
|
|
func ollie_on_chat_update(cb C.ollie_chat_cb) {
|
|
chatCB = cb
|
|
}
|
|
|
|
// --- Memory ---
|
|
|
|
//export ollie_free
|
|
func ollie_free(ptr *C.char) {
|
|
C.free(unsafe.Pointer(ptr))
|
|
}
|
|
|
|
// main is required for c-shared but never called.
|
|
func main() {}
|