From 20386977c1adea41e728d4b639fe51f5186cef0d Mon Sep 17 00:00:00 2001 From: pdfinn Date: Fri, 23 Jan 2026 09:15:19 +0700 Subject: [PATCH] feat: Add stream/ask file to trigger streaming requests - Add StreamAskFile to start streaming via write to stream/ask - Read chunks from stream/chunk as they arrive - Update documentation with streaming examples and verified tests - Update _example file with correct streaming instructions Streaming workflow: 1. Write prompt to stream/ask to start streaming 2. Read from stream/chunk to get chunks (blocks until available) 3. EOF returned when stream completes Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 9 +++++++++ README.md | 35 +++++++++++++++++++++++++++++++-- internal/llmfs/example.go | 13 ++++++++----- internal/llmfs/root.go | 1 + internal/llmfs/stream.go | 41 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1681442a6abe7c14f21277df5678d4b5f301f6aa..47e77383332715a6ad035ceb44fde25838cdeaf4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -313,6 +313,15 @@ The following scenarios have been tested and verified working: - [x] `echo 'prompt' > /n/llm/ask` followed by `cat /n/llm/ask` - Full LLM interaction works - [x] LLM correctly identifies client as Inferno OS when asked +### Streaming (plan9port) +- [x] `ls stream` - Lists `ask` and `chunk` files +- [x] `echo "prompt" | 9p write stream/ask` - Starts streaming request +- [x] `9p read stream/chunk` - Returns streamed chunks +- [x] Multiple chunks received for longer responses +- [x] EOF returned when stream completes +- [x] Short response ("Write a haiku") streams correctly +- [x] Long response ("Count 1 to 20") streams all content + ## Future Enhancements - [ ] Multiple conversation support (via subdirectories) diff --git a/README.md b/README.md index 7827b9fd54d7eec90c7c97c5a5e341f26d3ec145..d617983b8e8feed5f5a86160e1cd40e01781a177 100644 --- a/README.md +++ b/README.md @@ -150,22 +150,53 @@ cat /mnt/llm/_example ├── context # Read: conversation history; Write: add system message ├── _example # Read-only: usage examples └── stream/ # Streaming interface - └── chunk # Read blocks until next chunk, EOF on completion + ├── ask # Write-only: starts a streaming request + └── chunk # Read-only: blocks until next chunk, EOF on completion ``` ### File Behaviors | File | Read | Write | |------|------|-------| -| `ask` | Returns last LLM response | Sends prompt to LLM, stores response | +| `ask` | Returns last LLM response | Sends prompt to LLM (sync), stores response | | `model` | Returns current model name | Sets model for subsequent requests | | `temperature` | Returns current temperature | Sets temperature (0.0-2.0) | | `tokens` | Returns last response token count | Permission denied | | `new` | Permission denied | Any write resets conversation state | | `context` | Returns JSON conversation history | Appends system message to context | | `_example` | Returns usage examples | Permission denied | +| `stream/ask` | Permission denied | Starts a streaming request | | `stream/chunk` | Blocks until next chunk, returns it | Permission denied | +## Streaming + +For long responses, use the streaming interface to see output as it's generated: + +```bash +# Start a streaming request (using 9p tool) +echo "Write a poem about the moon" | 9p -a localhost:5640 write stream/ask & + +# Read chunks as they arrive +while chunk=$(9p -a localhost:5640 read stream/chunk 2>/dev/null); do + [ -z "$chunk" ] && break + printf "%s" "$chunk" +done +``` + +With a mounted filesystem: + +```bash +# Start streaming in background +echo "Explain quantum computing" > /mnt/llm/stream/ask & + +# Read chunks +while read -r chunk < /mnt/llm/stream/chunk 2>/dev/null; do + printf "%s" "$chunk" +done +``` + +**Note:** Start reading chunks immediately after writing to `stream/ask`. If you wait too long, the stream may complete and you'll get EOF. + ## Shell Scripting ```bash diff --git a/internal/llmfs/example.go b/internal/llmfs/example.go index c99c98618134d8c2b5481279b31962492c415704..61ff95d23f242028f8eb677de1db08771a0667e7 100644 --- a/internal/llmfs/example.go +++ b/internal/llmfs/example.go @@ -25,9 +25,11 @@ Conversation Management: Token Usage: cat tokens # View tokens from last response -Streaming (Advanced): - echo "Tell me a story" > ask # Start generating - cat stream/chunk # Read chunks as they arrive (blocks) +Streaming: + echo "Tell me a story" > stream/ask # Start streaming request + cat stream/chunk # Read next chunk (blocks until available) + # Keep reading stream/chunk until EOF for full response + # Note: Read chunks immediately after writing to stream/ask Shell Scripting: #!/bin/sh @@ -46,14 +48,15 @@ Environment: ANTHROPIC_API_KEY must be set when starting the server Files: - ask Read/write: prompt goes in, response comes out + ask Read/write: prompt goes in, response comes out (sync) model Read/write: current model name temperature Read/write: sampling temperature (0.0-2.0) tokens Read-only: token count from last response new Write-only: any write resets conversation context Read: JSON history; Write: add system message _example Read-only: this help text - stream/chunk Read-only: streaming chunks (blocking) + stream/ask Write-only: starts a streaming request + stream/chunk Read-only: returns next chunk (blocks), EOF when done ` // NewExampleFile creates the _example file with usage examples diff --git a/internal/llmfs/root.go b/internal/llmfs/root.go index c59448ac84e8b3a206e7e19a2be495ca2875565d..e0b5d445be4de249f40b5546e3049abaafceea1d 100644 --- a/internal/llmfs/root.go +++ b/internal/llmfs/root.go @@ -21,6 +21,7 @@ func NewRoot(client *llm.Client) protocol.Dir { // Add stream directory streamDir := protocol.NewStaticDir("stream") + streamDir.AddChild(NewStreamAskFile(client)) streamDir.AddChild(NewChunkFile(client)) root.AddChild(streamDir) diff --git a/internal/llmfs/stream.go b/internal/llmfs/stream.go index 99302e1fd32168f41c7c6f27f90d7e25bba489ed..63647f9430730272c070082b87705ee331e105b6 100644 --- a/internal/llmfs/stream.go +++ b/internal/llmfs/stream.go @@ -1,7 +1,9 @@ package llmfs import ( + "context" "io" + "strings" "github.com/NERVsystems/llm9p/internal/llm" "github.com/NERVsystems/llm9p/internal/protocol" @@ -51,3 +53,42 @@ func (f *ChunkFile) Stat() protocol.Stat { s.Length = 0 return s } + +// StreamAskFile starts a streaming request +// Write a prompt to start streaming, then read chunks from stream/chunk +type StreamAskFile struct { + *protocol.BaseFile + client *llm.Client +} + +// NewStreamAskFile creates the stream/ask file +func NewStreamAskFile(client *llm.Client) *StreamAskFile { + return &StreamAskFile{ + BaseFile: protocol.NewBaseFile("ask", 0222), // write-only + client: client, + } +} + +func (f *StreamAskFile) Read(p []byte, offset int64) (int, error) { + return 0, protocol.ErrPermission +} + +func (f *StreamAskFile) Write(p []byte, offset int64) (int, error) { + prompt := strings.TrimSpace(string(p)) + if prompt == "" { + return len(p), nil + } + + // Start streaming - chunks will be available via stream/chunk + err := f.client.StartStream(context.Background(), prompt) + if err != nil { + // Return error to indicate stream failed to start + return 0, err + } + + return len(p), nil +} + +func (f *StreamAskFile) Stat() protocol.Stat { + return f.BaseFile.Stat() +}