f42d1594ef
The extraction half. internal/email.Extractor asks the resident Qwen3-1.7B, under a GBNF grammar, what one message requires of him, and returns at most three short candidates with an optional date. Everything it can produce is a row in `tasks` with status "candidate", written through the intake seam #130 built for exactly this (Source "email:<mailbox>", Evidence = the subject line). No reminder, no fact, no note, no calendar event. That bound is the design: a reminder FIRES, so a 1.7B misreading "встреча была в четверг" as a future appointment would wake him up about it, whereas a wrong candidate is a line he dismisses in one click. A due date the model read out of the mail is stored on the candidate, where no scheduler reads it — the review page sorts by it. Relative wording ("до пятницы") is deliberately left in the text rather than resolved to a date the model would get wrong. The prompt is written against the two things a small model does here: it summarises when asked to extract, and it invents an obligation out of a polite closing line. Hence the demand for a verb phrase, and an explicit empty array — most mail contains no task, and a model with no way to say "nothing" says something. Wiring: core owns extraction because llama-server lives in core's process, so the reader hands messages over a new ipc.MethodIngestMail. It is a Server hook (like StepUp/UnlockFn), not a CoreAPI method — not a store operation, and no CoreAPI implementation should have to carry it. The hook stays nil without an `email` config block or without a llama-server phraser, so the method answers ErrUnknownMethod: off unless configured, twice over. There is no keyword fallback on purpose — "the subject became a task" is a mailbox rendered as a to-do list, not extraction. Privacy: junk is refused before the model is called, mail text is never search input, extraction errors carry byte counts rather than the reply, the stored evidence is a truncated subject, and the log line names the mailbox and the UID only.
150 lines
5.6 KiB
Go
150 lines
5.6 KiB
Go
package ipc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Method — one RPC verb. The set is intentionally small: it mirrors exactly
|
|
// what a module legitimately needs from core state, and nothing more. Adding
|
|
// a method is a core-authority change (every method is a new thing a module
|
|
// can ask for); do it deliberately.
|
|
type Method string
|
|
|
|
const (
|
|
MethodWriteFact Method = "write_fact"
|
|
MethodLatestFact Method = "latest_fact"
|
|
MethodLatestFactBySource Method = "latest_fact_by_source"
|
|
MethodSince Method = "since"
|
|
MethodPresence Method = "presence"
|
|
MethodCreateReminder Method = "create_reminder"
|
|
MethodMarkReminder Method = "mark_reminder"
|
|
MethodListReminders Method = "list_reminders"
|
|
MethodRecordNudge Method = "record_nudge"
|
|
MethodResolveNudge Method = "resolve_nudge"
|
|
MethodRecentOutcomes Method = "recent_outcomes"
|
|
MethodRecentFacts Method = "recent_facts"
|
|
MethodCalendarEvents Method = "calendar_events"
|
|
MethodRecentNudges Method = "recent_nudges"
|
|
MethodWriteNote Method = "write_note"
|
|
MethodQueryNotes Method = "query_notes"
|
|
MethodRecentNotes Method = "recent_notes"
|
|
MethodProposeTool Method = "propose_tool"
|
|
MethodEnableTool Method = "enable_tool"
|
|
MethodDisableTool Method = "disable_tool"
|
|
MethodAssertStepUp Method = "assert_stepup"
|
|
MethodStoreEncryptionKey Method = "store_encryption_key"
|
|
MethodUnlock Method = "unlock"
|
|
MethodLookupTool Method = "lookup_tool"
|
|
MethodListTools Method = "list_tools"
|
|
MethodDeleteTool Method = "delete_tool"
|
|
MethodListProposedRoutines Method = "list_proposed_routines"
|
|
MethodDismissProposedRoutine Method = "dismiss_proposed_routine"
|
|
MethodAcceptProposedRoutine Method = "accept_proposed_routine"
|
|
MethodRevertFact Method = "revert_fact"
|
|
MethodTickTrace Method = "tick_trace"
|
|
MethodMorningStatus Method = "morning_status"
|
|
MethodDayPlan Method = "day_plan"
|
|
MethodChat Method = "chat"
|
|
MethodCaptureTask Method = "capture_task"
|
|
MethodListTasks Method = "list_tasks"
|
|
MethodSetTaskStatus Method = "set_task_status"
|
|
MethodIngestMail Method = "ingest_mail"
|
|
)
|
|
|
|
// Request — one frame from module to core. Params is the JSON-encoded argument
|
|
// struct for Method (see api.go for the per-method shapes). The server
|
|
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
|
|
type Request struct {
|
|
Method Method `json:"m"`
|
|
Params json.RawMessage `json:"p,omitempty"`
|
|
}
|
|
|
|
// Response — one frame from core back to module. Exactly one of Result/Error
|
|
// is set. Result is the JSON-encoded return value of the method (might be a
|
|
// scalar, a struct, or null for void methods).
|
|
type Response struct {
|
|
Result json.RawMessage `json:"r,omitempty"`
|
|
Error *RpcError `json:"e,omitempty"`
|
|
}
|
|
|
|
// RpcError — a typed wire error. Code is one of the sentinel codes below;
|
|
// the client rehydrates it into the matching package sentinel so callers can
|
|
// use errors.Is like they would in-process (core's contract is the same on
|
|
// both sides of the wire — the boundary shouldn't change error semantics).
|
|
type RpcError struct {
|
|
Code string `json:"c"`
|
|
Message string `json:"m,omitempty"`
|
|
}
|
|
|
|
func (e *RpcError) Error() string {
|
|
if e.Message != "" {
|
|
return fmt.Sprintf("ipc: %s: %s", e.Code, e.Message)
|
|
}
|
|
return fmt.Sprintf("ipc: %s", e.Code)
|
|
}
|
|
|
|
// Sentinel codes. Stable over the wire — do not rename. Mirror the package
|
|
// sentinels in api.go 1:1. The string is the contract.
|
|
const (
|
|
codeNoFact = "no_fact"
|
|
codeConfidence = "confidence"
|
|
codeVoidsMissing = "voids_missing"
|
|
codeNudgeNotFound = "nudge_not_found"
|
|
codeNudgeOutcome = "nudge_outcome"
|
|
codeReminderMissing = "reminder_not_found"
|
|
codeReminderState = "reminder_state"
|
|
codeToolNotFound = "tool_not_found"
|
|
codeUnknownMethod = "unknown_method"
|
|
codeBadParams = "bad_params"
|
|
codeForbidden = "forbidden"
|
|
codeInternal = "internal"
|
|
)
|
|
|
|
// codeOf maps a server-side sentinel to its wire code. Anything not matched
|
|
// is codeInternal — we never leak internal Go error text to a module; it
|
|
// gets a generic "internal" and the daemon logs the real error server-side.
|
|
func codeOf(err error) string {
|
|
switch {
|
|
case err == nil:
|
|
return ""
|
|
case errors.Is(err, ErrNoFact):
|
|
return codeNoFact
|
|
case errors.Is(err, ErrConfidence):
|
|
return codeConfidence
|
|
case errors.Is(err, ErrVoidsMissing):
|
|
return codeVoidsMissing
|
|
case errors.Is(err, ErrNudgeNotFound):
|
|
return codeNudgeNotFound
|
|
case errors.Is(err, ErrNudgeOutcome):
|
|
return codeNudgeOutcome
|
|
case errors.Is(err, ErrReminderNotFound):
|
|
return codeReminderMissing
|
|
case errors.Is(err, ErrReminderState):
|
|
return codeReminderState
|
|
case errors.Is(err, ErrToolNotFound):
|
|
return codeToolNotFound
|
|
case errors.Is(err, ErrUnknownMethod):
|
|
return codeUnknownMethod
|
|
case errors.Is(err, ErrBadParams):
|
|
return codeBadParams
|
|
case errors.Is(err, ErrForbidden):
|
|
return codeForbidden
|
|
default:
|
|
return codeInternal
|
|
}
|
|
}
|
|
|
|
// rpcErr builds the wire error for a server-side error. message is omitted
|
|
// for sentinel codes (the Code carries the meaning; no need to echo text the
|
|
// caller can re-derive from errors.Is) and included for internal/bad-params
|
|
// where the text is the actual diagnostic.
|
|
func rpcErr(err error) *RpcError {
|
|
c := codeOf(err)
|
|
if c == codeInternal || c == codeBadParams {
|
|
return &RpcError{Code: c, Message: err.Error()}
|
|
}
|
|
return &RpcError{Code: c}
|
|
}
|