Replace the blocking Chat() call in process() with
chatStreamWithFallback: ChatStream first, accumulate chunks, fall back
to non-stream Chat on connect failure or empty-stream failure.
Why: the non-streaming path blocked for the ENTIRE LLM generation (up
to the 180s HTTP timeout). Reasoning models thinking 60-120s plus AUTO
chain failover regularly exceeded it -> context canceled -> full turn
wasted. With streaming the first chunk arrives in ~1-3s and any
flowing token keeps the connection alive; total generation time is no
longer bounded by an overall timeout.
Compatibility (external behavior unchanged):
- process() signature/return values unchanged
- Aggregated events (EventReasoning / EventAgentLLMChain) still fire
once per turn with full text after stream completion - existing
plugin subscribers see identical payloads as before
- New incremental events EventReasoningDelta / EventContentDelta are
additive; old subscribers ignore unknown event types
- Tool execution loop, memory pipeline, stage pipeline untouched
Streaming details:
- Tool call fragments accumulated per OpenAI streaming convention:
id/name arrive on the first fragment, arguments as raw JSON string
shards across fragments; merged and parsed once at stream end
- normalizeStreamToolCalls keeps nameless argument shards (the
non-stream normalizer drops them); ToolCall gains RawArguments to
carry shard text
- Interrupt mid-stream returns partial content instead of discarding
the whole generation
Verified end-to-end against llmsproxy: plain chat streams correctly;
curl confirms tool-call shard wire format ({" + command" + :"date"}
-> {"command":"date"}); unit tests cover shard merging and
content/reasoning accumulation.
Rewrite LuaAdaptedProvider.ChatStream to match the maturity of
llmsproxy's streaming implementation:
HTTP layer:
- Dedicated stream HTTP client with no overall timeout (SSE must not
be cut by the 180s Chat timeout); only a 30s dial timeout
- Uses applyAdapterHeaders (supports build_headers dynamic signing
hook), matching the non-streaming Chat path
Non-200 response handling:
- New TransformError Lua hook (adapter.transform_error) for per-source
protocol knowledge in error messages
- Safe fallback truncation of raw error bodies (prevents HTML dump
leakage to clients)
SSE parsing enhancements:
- parseOpenAICompatibleStreamChunkFull: handles token usage in the
final chunk (prompt_tokens/prompt, total_tokens/total dual keys),
prompt cache detail fields, and empty-string finish_reason filtering
(sensenova sends "" on every chunk)
- Replaced old SSEScanner with bufio.Scanner (larger buffer, fewer
allocations)
Stream integrity:
- errorOnlyChunk detection: holds back the first chunk to reject
degenerate streams (e.g. zen free pool's finish_reason:"network_error"
with empty content) before any byte reaches the caller
- [DONE] dedup: adapters that already emit a terminating done chunk
with the real finish_reason don't get a second reason-less done
- Clean EOF sends a final Done:true if no done was seen
Struct changes:
- StreamChunk: added FinishReason and Usage fields for callers
- LuaAdaptedProvider: added streamClient (lazy) + streamMu
Tested: curl against llmsproxy SSE confirms reasoning_content parsing
is correct (delta.reasoning_content), usage chunk handling works, and
[DONE] termination is properly emitted.
The local llmsproxy AUTO chain tries 6+ slots across 3 tiers sequentially.
Each failed tier incurs busyWait (2s) + upstream timeout, so a full chain
exhaustion can exceed 120s. The llmsproxy logs showed 143 'context canceled'
errors for the homeagent key — the client gave up before the chain finished.
180s gives the chain enough room to complete before the client timeout fires.
Also remove stale backup files under /usr/local/bin/.
- Six-phase plan complete: webui/cli/healthcheck/pluginmgr/clawhubadapter
now interact with the kernel exclusively via internal/sdk interfaces;
all Configure() calls and package-level global injection removed
- buildSDK in internal/plugin/registry.go is the single assembly point
- Add internal/sdk/events.go exporting event types/constants
- Fix ProviderManager cooldown sharing: LuaAdaptedProvider.Name() now
returns the source name instead of lua_<adapter>, so multiple sources
sharing an adapter (single script load via shared VM AdapterCache) no
longer share failure-cooldown state
- Verified: build/vet/tests green, deployed to homeagent.service with
full plugin capability testing via local OpenAI-compatible mock
- process.go: only emit resp.Content on the first tool call per batch,
subsequent assistant messages use empty content (serialized as null)
- provider.go: MarshalJSON outputs null content when empty with tool_calls
to comply with DeepSeek/OpenAI expected format
- output.go: update parameter interface (payload/type/meta) to match
tool definitions (uncommitted from previous refactor)