ref: 974a684109c1333eccd8c89d519ea869ceec1836
dir: /ai/
#!/bin/rc
rfork e
wctl=/dev/null
if(test -w /dev/wctl)
wctl=/dev/wctl
if(~ $AI_MODEL '')
model=gpt-4o-mini
if not
model=$AI_MODEL
apiurl=https://api.openai.com/v1/responses
state=/tmp/ai.$pid
sessionlog=/tmp/ai.$pid.log
fn usage {
echo 'usage: ai [-m model] [question ...]' >[1=2]
exit usage
}
fn sigexit {
rm -rf $state
}
fn logmsg {
{
echo $*
} >>$sessionlog
}
fn logsection {
ifs='' {label=$1}
file=$2
{
echo '== '^$label^' =='
if(test -f $file){
if(test -s $file)
cat $file
if not
echo '(empty)'
}
if not
echo '(missing)'
echo
} >>$sessionlog
}
fn needkey {
ifs='' {apikey=`{auth/userpasswd -n 'service=openai host=api.openai.com user=api' >[2]/dev/null | sed -n '2p'}}
if(! ~ $apikey '')
status=()
if not {
echo 'ai: add an openai token to factotum' >[1=2]
echo 'ai: echo ''key proto=pass service=openai host=api.openai.com user=api !password=...'' >/mnt/factotum/ctl' >[1=2]
exit apikey
}
}
fn sysmsg {
cat <<'!'
You are ai, a small coding assistant running from rc on 9front.
You are helping inside the user's current source tree.
The user may ask about the current tree or about the local system.
You do not have direct file access. You must use the available tools.
Available tools:
tool files
Return a compact file listing for the current directory, with line counts.
Output format: one line per file, as "<path> <lines>".
tool man
page <manual page name>
Show a manual page from the local 9front system.
tool shell
cmd <plan9 rc command>
Run one command under Plan 9 rc in the current directory.
tool search
pattern <grep pattern>
Search files in the current directory.
tool read
path <relative file path>
start <1-based line number>
count <line count>
Read a slice of one file.
Rules:
- Ask for at most one tool per response.
- Use only relative paths.
- Gather direct evidence before answering substantive questions.
- Prefer the tool that directly answers the user's question over indirect file inspection.
- Use repository files only when they are relevant to the user's question.
- tool shell runs Plan 9 rc, not sh or bash.
- In Plan 9 rc, $path is the command search path; `ls $path` lists available commands.
- Use Plan 9 commands and rc syntax when using tool shell.
- Use tool read to inspect implementation before making claims about behavior.
- Do not answer from file names, target names, or build metadata alone when implementation files are available.
- Do not modify files through tool shell unless the user explicitly asks for changes.
- Do not invent file contents.
- Do not request write tools; edits are not implemented yet.
- When you are ready to answer the user, stop using tools.
Output format:
- For a tool request, output exactly:
tool files
or
tool man
page ...
or
tool shell
cmd ...
or
tool search
pattern ...
or
tool read
path ...
start ...
count ...
- For a final user-facing response, output exactly:
reply
<your response text>
Do not wrap the output in markdown fences.
!
}
fn planmsg {
cat <<'!'
You are ai, a small coding assistant running from rc on 9front.
Available tools: files, man, shell, search, read.
Write a short execution plan for the latest user request.
Focus on the user's request, not on these instructions.
Treat the latest user request as the only task to plan for.
Choose the smallest set of relevant tools.
The latest request may be about the repository or about the local system.
Do not use tools yet.
Do not answer the user yet.
Output format:
- Output exactly:
plan
<1-3 short lines>
Do not wrap the output in markdown fences.
!
}
fn listfiles {
ls
}
fn summary {
echo 'pwd'
pwd
echo
echo 'top level'
ls | sed 120q
echo
}
fn turnview {
if(test -f $state/turn)
cat $state/turn
if not
echo '(empty)'
}
fn mkprompt {
{
sysmsg
echo
echo 'Latest user request:'
cat $state/user
echo
echo 'Repository summary:'
summary
echo 'Prior conversation:'
if(test -f $state/history)
cat $state/history
if not
echo '(empty)'
echo
echo 'Current turn work:'
turnview
echo
} >$state/prompt
}
fn mkplanprompt {
{
planmsg
echo
echo 'Latest user request:'
cat $state/user
} >$state/prompt
}
fn mkrequest {
ifs='' {
modelq=`{jquote $model}
inputq=`{jquote < $state/prompt}
}
echo -n '{"model":'^$modelq^',"input":'^$inputq^',"store":false}' >$state/request
}
fn httpmeta {
phase=$1
{
echo 'phase '^$phase
echo 'method POST'
echo 'url '^$apiurl
echo 'header Content-Type: application/json'
echo 'header Authorization: Bearer [redacted]'
echo 'body '^$state/request
} >$state/http
}
fn apicall {
phase=$1
rm -f $state/errorbody
httpmeta $phase
logsection $phase^-http $state/http
hget \
-e $state/errorbody \
-r 'Content-Type: application/json' \
-r 'Authorization: Bearer '^$apikey \
-P $apiurl \
<$state/request >$state/response
httpstatus=$status
{
if(~ $httpstatus '')
echo 'status ok'
if not
echo 'status '^$"httpstatus
} >$state/httpstatus
logsection $phase^-http-status $state/httpstatus
logsection $phase^-response $state/response
if(test -f $state/errorbody)
logsection $phase^-errorbody $state/errorbody
status=$httpstatus
}
fn callmodel {
mkprompt
mkrequest
logsection prompt $state/prompt
logsection model-request $state/request
echo 'thinking...' >[1=2]
apicall model
}
fn callplan {
mkplanprompt
mkrequest
logsection plan-prompt $state/prompt
logsection plan-request $state/request
echo 'planning...' >[1=2]
apicall plan
}
fn extracttext {
errtype=`{jget type error < $state/response >[2]/dev/null}
if(~ $errtype object){
if(jget key error < $state/response | jget key message | jget str >$state/error >[2]/dev/null)
cat $state/error >[1=2]
if not
cat $state/response >[1=2]
status=error
}
if not
if(! jget key output < $state/response \
| jget idx 0 \
| jget key content \
| jget idx 0 \
| jget str text >$state/model >[2]/dev/null){
echo 'ai: could not decode model response' >[1=2]
cat $state/response >[1=2]
status=decode
}
if not
status=()
}
fn cleanmodel {
sed 's/\r$//' $state/model >$state/model.clean
mv $state/model.clean $state/model
}
fn cleantool {
sed '/^$/d' $state/model >$state/model.clean
mv $state/model.clean $state/model
}
fn field {
ifs='' {value=`{grep '^'^$1^' ' $2 | sed 1q | sed 's/^[^ ]* //' | sed 's/[ ]*$//' | sed 's/^"\(.*\)"$/\1/' | sed 's/\r$//'}}
echo -n $value
}
fn goodpath {
switch($1){
case '' /* ../* */../* */.. ..
status=badpath
case *
status=()
}
}
fn toolfiles {
for(f in `{listfiles}){
if(test -f $f){
n=`{wc -l < $f}
echo $f^' '^$n
}
}
}
fn toolman {
page=$1
if(~ $page ''){
echo 'missing page'
status=page
}
if not
man $page | sed 200q
}
fn toolshell {
ifs=' ' {cmd=$*}
if(~ $cmd ''){
echo 'missing command'
status=command
}
if not {
rc -c $cmd
cmdstatus=$status
if(~ $cmdstatus '')
echo 'status 0'
if not
echo 'status '^$"cmdstatus
status=()
}
}
fn toolsearch {
ifs=' ' {pat=$*}
files=()
if(~ $pat ''){
echo 'missing pattern'
status=pattern
}
if not
files=`{listfiles}
if(~ $#files 0){
echo 'no files'
status=nofiles
}
if not
@{
for(f in $files)
if(test -f $f)
grep -n $pat $f >[2]/dev/null
} | sed 200q
}
fn toolread {
file=$1
start=$2
count=$3
if(~ $start '')
start=1
if(~ $count '')
count=120
if(! goodpath $file){
echo 'bad path'
status=badpath
}
if not {
if(! test -f $file){
echo 'missing file'
status=missing
}
if not {
end=`{echo $start $count + 1 - p | dc}
sed -n $start^','^$end^'{=;p;}' $file | sed 'N;s/\n/: /'
}
}
}
fn adduser {
{
echo '== user =='
cat $state/user
echo
} >>$state/history
}
fn addtool {
{
echo '== tool '^$1^' =='
cat $2
echo
} >>$state/turn
}
fn addreply {
{
echo '== assistant =='
cat $1
echo
} >>$state/history
}
fn addplan {
{
echo '== plan =='
cat $1
echo
} >>$state/turn
}
fn setuser {
echo $* >$state/user
rm -f $state/lasttool
rm -f $state/plan
rm -f $state/turn
logsection user $state/user
}
fn planturn {
if(! callplan)
status=plan
if not {
if(! extracttext)
status=plan
if not {
cleanmodel
logsection plan-model $state/model
kind=`{sed 1q $state/model | sed 's/[ ].*//'}
switch($kind){
case plan
sed 1d $state/model >$state/plan
case *
cp $state/model $state/plan
}
if(! test -s $state/plan)
echo '(empty plan)' >$state/plan
echo 'plan:' >[1=2]
cat $state/plan >[1=2]
addplan $state/plan
status=()
}
}
}
fn runturn {
done=no
for(step in 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){
if(~ $done no){
logmsg '== round '^$step^' =='
if(! callmodel)
done=error
if not {
if(! extracttext)
done=error
if not {
cleanmodel
logsection model $state/model
type=`{sed 1q $state/model | sed 's/[ ].*//'}
logmsg 'type '^$type
switch($type){
case reply
sed 1d $state/model >$state/reply
if(! test -s $state/reply)
echo '(empty reply)' >$state/reply
cat $state/reply
addreply $state/reply
done=yes
case tool
cleantool
logsection tool-request $state/model
name=`{sed 1q $state/model | sed 's/^[^ ]*[ ]*//'}
logmsg 'tool '^$name
repeat=no
if(test -f $state/lasttool)
if(cmp -s $state/model $state/lasttool >[2]/dev/null){
repeat=yes
}
if(~ $repeat yes){
echo 'repeat tool request ignored; choose a different tool or reply.' >$state/toolout
addtool repeat $state/toolout
logsection toolout $state/toolout
}
if(~ $repeat no){
cp $state/model $state/lasttool
switch($name){
case files
echo 'tool: files' >[1=2]
toolfiles | sed 200q >$state/toolout >[2=1]
addtool files $state/toolout
logsection toolout $state/toolout
case man
ifs='' {page=`{field page $state/model}}
echo 'tool: man '^$page >[1=2]
toolman $page >$state/toolout >[2=1]
addtool man $state/toolout
logsection toolout $state/toolout
case shell
ifs='' {cmd=`{field cmd $state/model}}
echo 'tool: shell '^$cmd >[1=2]
toolshell $cmd >$state/toolout >[2=1]
addtool shell $state/toolout
logsection toolout $state/toolout
case search
ifs='' {pat=`{field pattern $state/model}}
echo 'tool: search '^$pat >[1=2]
toolsearch $pat >$state/toolout >[2=1]
addtool search $state/toolout
logsection toolout $state/toolout
case read
file=`{field path $state/model}
start=`{field start $state/model}
count=`{field count $state/model}
echo -n 'tool: read ' >[1=2]
echo -n $file >[1=2]
echo -n ' ' >[1=2]
echo -n $start >[1=2]
echo -n ' ' >[1=2]
echo $count >[1=2]
toolread $file $start $count >$state/toolout >[2=1]
addtool read $state/toolout
logsection toolout $state/toolout
case *
cat $state/model
addreply $state/model
done=yes
}
}
case *
cat $state/model
addreply $state/model
done=yes
}
}
}
}
}
switch($done){
case yes
status=()
case error
status=runturn
case *
echo 'ai: too many tool rounds' >[1=2]
status=toomanytools
}
}
fn help {
echo '/help show this help'
echo '/reset clear the conversation'
echo '/context show the current repo summary'
echo '/quit exit'
}
while(! ~ $#* 0 && ~ $1 -*){
switch($1){
case -m
shift
if(~ $#* 0)
usage
model=$1
case *
usage
}
shift
}
mkdir $state || exit state
{
echo 'ai log'
echo 'pid '^$pid
echo 'model '^$model
echo 'pwd'
pwd
echo
} >$sessionlog
needkey
if(! ~ $#* 0){
setuser $*
adduser
if(! planturn)
echo 'ai: could not make plan; continuing' >[1=2]
runturn
exit $status
}
echo 'ai: terminal mode; /help for commands' >[1=2]
while() {
echo -n current >$wctl
echo -n top >$wctl
echo -n 'ai> ' >[1=2]
ifs='' {line=`{read}}
if(~ $#line 0)
exit
switch($line){
case /quit /exit
exit
case /help
help
case /reset
rm -f $state/history
echo 'conversation cleared'
case /context
summary
case ''
# ignore
case *
setuser $"line
adduser
if(! planturn)
echo 'ai: could not make plan; continuing' >[1=2]
runturn
}
}