Support script paths for system_prompt and scheduled prompts #35

Open
opened 2026-07-05 13:36:31 +08:00 by weiwen · 0 comments
Owner

What to build

Let both pi.system_prompt and schedule[].prompt point at an executable script instead of only holding literal text. When a configured value is a script path, Evie executes it at point-of-use and uses its stdout as the prompt. This enables programmatic/templated prompt generation (e.g. assembling today's context) instead of a fixed string.

Deliver in one PR, sequenced as commits:

Commit 1 — shared resolution foundation (src/prompt_script.rs)

  • enum PromptSource { Literal(String), Script(PathBuf) }.
  • async fn resolve(&PromptSource, &PromptContext) -> Result<String> using tokio::process::Command with .kill_on_drop(true) + tokio::time::timeout.
  • Detection: a configured value is a script iff it ends in .sh/.py and the file exists after path resolution. Ends in .sh/.py but missing → hard config error. Otherwise → literal (existing behavior).
  • Path resolution: tilde-expand; resolve relative paths against the config file's directory; store absolute.
  • Runtime contract: shebang-driven execution (the file is exec'd directly — extension is detection only, not interpreter dispatch); +x required. CWD = notes directory. Inherit parent env + inject EVIE_TRIGGER (system_prompt|schedule), EVIE_CHAT_ID, EVIE_NOTES_DIR. No args, no stdin. stdout → prompt.
  • Failure (no silent fallback): non-zero exit OR empty stdout OR timeout → hard failure for that invocation. Capture stderr always; on failure log truncated (~2KB) with exit code + surface. Never leak stderr into chat.
  • Config: new top-level prompt_script_timeout_secs, default 10, #[serde(default)].
  • Config-load classification + validation: post-parse pass classifies each prompt field into PromptSource; for a detected script path, validate exists + executable (do NOT execute). Literal prompts keep the existing non-empty check.

Commit 2 — scheduled-prompt surface

  • scheduler_task resolves the entry's PromptSource at each cron fire (with EVIE_TRIGGER=schedule, EVIE_CHAT_ID = the target chat) before pushing to the delivery channel; DeliveryItem stays (ChatId, String).
  • On resolve failure: log warn, skip the fire, continue the loop.

Commit 3 — system-prompt surface

  • SessionManagerConfig.system_prompt becomes a PromptSource; resolve per pi spawn (EVIE_TRIGGER=system_prompt).
  • On resolve failure: abort the spawn and surface a generic error to chat per expose_errors (no raw stderr in chat).
  • Update default-config doc comments to describe script support.

Acceptance criteria

  • A .sh/.py scheduled prompt fires on schedule and delivers the script's stdout; a broken/timing-out/empty script logs a warn and skips the fire without crashing the scheduler.
  • A .sh/.py system prompt is executed per pi spawn and passed to pi; a broken script aborts the spawn and yields a clean error to chat under expose_errors (raw stderr only in logs).
  • Scripts run with CWD = notes dir, parent env inherited, and EVIE_TRIGGER/EVIE_CHAT_ID/EVIE_NOTES_DIR set; interpreter comes from the shebang.
  • A .sh/.py value whose file is missing fails config load; a value ending otherwise is treated as literal (existing behavior unchanged).
  • A detected script path that is not executable fails config load; config load does not execute any script.
  • Relative script paths resolve against the config file's directory; ~ is expanded.
  • prompt_script_timeout_secs defaults to 10 and bounds each execution; existing configs without the field still load.
  • Tests cover: detection vs. literal, missing-file error, non-executable error, successful resolution, non-zero/empty/timeout failure, and env/CWD wiring.

Blocked by

None - can start immediately

## What to build Let both `pi.system_prompt` and `schedule[].prompt` point at an executable script instead of only holding literal text. When a configured value is a script path, Evie executes it at point-of-use and uses its stdout as the prompt. This enables programmatic/templated prompt generation (e.g. assembling today's context) instead of a fixed string. Deliver in one PR, sequenced as commits: **Commit 1 — shared resolution foundation** (`src/prompt_script.rs`) - `enum PromptSource { Literal(String), Script(PathBuf) }`. - `async fn resolve(&PromptSource, &PromptContext) -> Result<String>` using `tokio::process::Command` with `.kill_on_drop(true)` + `tokio::time::timeout`. - **Detection:** a configured value is a script iff it ends in `.sh`/`.py` **and** the file exists after path resolution. Ends in `.sh`/`.py` but missing → hard config error. Otherwise → literal (existing behavior). - **Path resolution:** tilde-expand; resolve relative paths against the config file's directory; store absolute. - **Runtime contract:** shebang-driven execution (the file is exec'd directly — extension is detection only, not interpreter dispatch); `+x` required. CWD = notes directory. Inherit parent env + inject `EVIE_TRIGGER` (`system_prompt`|`schedule`), `EVIE_CHAT_ID`, `EVIE_NOTES_DIR`. No args, no stdin. stdout → prompt. - **Failure (no silent fallback):** non-zero exit OR empty stdout OR timeout → hard failure for that invocation. Capture stderr always; on failure log truncated (~2KB) with exit code + surface. Never leak stderr into chat. - **Config:** new top-level `prompt_script_timeout_secs`, default 10, `#[serde(default)]`. - **Config-load classification + validation:** post-parse pass classifies each prompt field into `PromptSource`; for a detected script path, validate exists + executable (do NOT execute). Literal prompts keep the existing non-empty check. **Commit 2 — scheduled-prompt surface** - `scheduler_task` resolves the entry's `PromptSource` at each cron fire (with `EVIE_TRIGGER=schedule`, `EVIE_CHAT_ID` = the target chat) **before** pushing to the delivery channel; `DeliveryItem` stays `(ChatId, String)`. - On resolve failure: log `warn`, skip the fire, continue the loop. **Commit 3 — system-prompt surface** - `SessionManagerConfig.system_prompt` becomes a `PromptSource`; resolve per `pi` spawn (`EVIE_TRIGGER=system_prompt`). - On resolve failure: abort the spawn and surface a generic error to chat per `expose_errors` (no raw stderr in chat). - Update default-config doc comments to describe script support. ## Acceptance criteria - [ ] A `.sh`/`.py` scheduled prompt fires on schedule and delivers the script's stdout; a broken/timing-out/empty script logs a `warn` and skips the fire without crashing the scheduler. - [ ] A `.sh`/`.py` system prompt is executed per `pi` spawn and passed to `pi`; a broken script aborts the spawn and yields a clean error to chat under `expose_errors` (raw stderr only in logs). - [ ] Scripts run with CWD = notes dir, parent env inherited, and `EVIE_TRIGGER`/`EVIE_CHAT_ID`/`EVIE_NOTES_DIR` set; interpreter comes from the shebang. - [ ] A `.sh`/`.py` value whose file is missing fails config load; a value ending otherwise is treated as literal (existing behavior unchanged). - [ ] A detected script path that is not executable fails config load; config load does not execute any script. - [ ] Relative script paths resolve against the config file's directory; `~` is expanded. - [ ] `prompt_script_timeout_secs` defaults to 10 and bounds each execution; existing configs without the field still load. - [ ] Tests cover: detection vs. literal, missing-file error, non-executable error, successful resolution, non-zero/empty/timeout failure, and env/CWD wiring. ## Blocked by None - can start immediately
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie#35
No description provided.