Class: Rubino::Jobs::Handlers::DistillSkillJob

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/jobs/handlers/distill_skill_job.rb

Overview

Variant B — deterministic post-turn skill distillation.

Enqueued from Interaction::Lifecycle#enqueue_post_turn_jobs alongside ExtractMemoryJob. The GATE is fully deterministic (no model call):

- the run produced a non-empty final assistant answer (succeeded), AND
- the turn used >= TOOL_THRESHOLD tool calls (mirrors the reference "5+"), AND
- no existing skill already covers the work (kept simple here:
  no skill whose name/description shares a salient keyword with the
  user's task — a fresh skills dir always passes).

Only on a gate-PASS do we spend ONE auxiliary-model call to distil the just-finished transcript into a SKILL.md candidate, which we then write. So: +1 LLM call per gate-pass, 0 otherwise.

Constant Summary collapse

TOOL_THRESHOLD =
Integer(ENV.fetch("RA_DISTILL_TOOL_THRESHOLD", "5"))
NAME_RE =
/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
DISTILL_SYSTEM =
<<~SYS
  You distil a just-finished agent task into a REUSABLE skill, or decline.
  You are given the user's task and a transcript of the tools the agent ran
  and its final answer. If — and only if — the work was a complex, multi-step,
  REPEATABLE procedure that would help future similar tasks, output a skill.
  If it was trivial, one-off, or not generalizable, decline.

  Output ONLY a JSON object, no prose:
  {"create": true, "name": "<kebab-case, <=64 chars>",
   "description": "<one line: what it's for and WHEN it applies>",
   "body": "<markdown: # Title then the proven step-by-step instructions, commands, pitfalls — generalized, not hard-coded to this one input>"}
  or {"create": false, "reason": "<why not skill-worthy>"}
SYS

Instance Method Summary collapse

Instance Method Details

#perform(payload) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rubino/jobs/handlers/distill_skill_job.rb', line 40

def perform(payload)
  session_id = payload[:session_id] || payload["session_id"]
  return unless session_id

  messages = Session::Store.new.for_session(session_id)
  return unless gate_passes?(messages)

  candidate = distill(messages)
  return unless candidate && candidate["create"] == true

  write_skill(candidate)
rescue StandardError => e
  Rubino.logger.warn(event: "jobs.distill_skill.error", error_class: e.class.name, message: e.message)
  nil
end