@projectpac/source-intents
v0.5.2
Published
Part of PAC: @projectpac/source-intents.
Readme
@projectpac/source-intents
What the principal wants done, as one source this adapter keeps in its own folder: intents a model extracted from the other sources on the node -- the coding-agent history and the conversation notes -- kept up to date as that material grows, each resting on short verbatim excerpts of what was said, and never the material itself.
Why a third source over the other two's material
agent-history reads the transcripts and answers who this person is.
conversation-notes mirrors a file a skill inside Claude writes and answers what
they talked about this week. Neither answers the question a flow could act on
without being asked: what does this person want done? That is what this keeps.
It reads nothing itself. Each source it derives from implements
@projectpac/material -- units(), which lists what moves as one thing, and
read(), which hands one unit over as observations a model may cite -- and this
plugin asks. It opens no file the principal's material lives in, stages
nothing, and carries no readers of its own. That is why it is not a consent of
its own on the capture step but comes along with whichever source is chosen:
installing a source is the consent to read that source, and a node that
installs this without one has nothing to extract from and says so.
The inputs
An input is a row in the config naming a source by the service key it claims:
{
"inputs": [
{ "id": "agent-history", "service": "agentHistory" },
{ "id": "conversation-notes", "service": "conversationNotes" }
]
}Those two are the default. Each is reached from an optional child scope rather than injected, the way the flows reach the router: a node with only one of them still runs this, and one that installs the other later contributes it on the next pass. A source that is not installed contributes no units and one log line. A third kind of material is a source that implements the contract and one more row here -- no code in this plugin.
| input | material | a unit | evidence is |
| -------------------- | ------------------------------------------------- | -------------- | --------------------- |
| agent-history | Claude Code transcripts, through its own readers | a session file | user or assistant |
| conversation-notes | the notes the daily-profile skill writes hourly | one note | summary |
A unit can be told apart without being read -- what stat says of a session
file, a digest of a day's text. A pass lists every unit, reads only the ones
that moved, and packs whole units into chunks, so a session that grew or a
note the skill added dirties exactly one chunk and every other slice, already
paid for, stands. The one thing this plugin does to what it is handed is defuse the
intelligence loop's own delimiters, because it is the plugin that puts the text
in a prompt and the loop refuses a context carrying one. A summary is an
account an assistant wrote about the person's conversations, not their words;
the model is told so, twice, and the evidence says so.
What an intent is
The shape is a General User Model's proposition (arXiv 2505.10831) bent to one question, with what a flow needs to act on the answer.
| field | what it is |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| id | twelve hex characters, minted here and never by the model |
| intent | one sentence, in the person's own terms, specific enough to act on |
| reasoning | how the evidence supports it; describes, never quotes |
| basis | explicit when the person asked in as many words, inferred when it is a fair reading |
| confidence | 1 to 10, asked of the model directly, which GUM finds calibrates better than anything read off its logits |
| status | open, done or dropped |
| projects | the sources' names for the directories the sessions ran in, matched rather than trusted |
| evidence | up to eight citations: which input, which unit, which message, whose words, when, and the excerpt |
| firstSeen, lastSeen | the earliest and latest time the material showed it, from the material's own timestamps |
| revised | a digest of the pass that last changed the row |
Three things GUM does are left out on purpose. Its retrieval -- BM25, decay, a
classifier labelling pairs identical or similar -- is one revise run handed the
whole list, because the list fits in one context. Its decay is the status
the revise run sets instead, with the timestamps beside it. And its "at least
five propositions" quota is gone: an empty answer is the right one for a slice
that shows nothing, and a quota manufactures work out of an ordinary
conversation.
Propose, then revise
Each changed chunk gets one run. It answers with up to twelve intents, each citing one to three messages by id with an excerpt copied verbatim. A citation is checked rather than trusted: the excerpt has to be a substring of the message it names, or it is dropped, and an intent left with no citation that passes is dropped with it. Nothing this adapter writes down as evidence is something the model composed.
Then one run is handed the list as it stands and every fresh proposal, and
answers with what should change: a row with an existing id is that intent,
updated; a row with none is new; several proposals for one outcome become one
row. What the model may not do is enforced in merge.ts rather than asked for.
An id counts only if the run was shown it. A row the answer leaves out is kept
exactly as it was, so leaving one out is never how to remove it. A new row has
to draw on a proposal, because the run may consolidate but not originate. And a
done or dropped intent reopens only on evidence newer than the last time it
was seen, so a slice re-read for some other reason cannot revive what the
person has since finished.
With nothing held and one slice proposing, the revise run is skipped: it would only be asked to repeat the propose run's answer, and dedupe within one slice is that run's own job.
What comes out
One source, intents: the open rows, newest first. Done and dropped rows
are the principal's history rather than work, so they stay in the record store
for the next revise and out of the file a flow can stake.
Two verbs, the same two the other sources offer: inventory() answers with the
name and a sentence about it -- counts, never a row -- and stage() copies the
file into the calling plugin's folder and answers with a relative path. That
is what a flow hands to a box. No route reads the list out, for the same reason
no route reads the profile out: it is a source. And it offers no Material of
its own: the list is a model's conclusions, and what can be reasoned over is
the sources this one reads.
Running it
It runs on its own: a first pass a minute after activation, then one every
everyMinutes, an hour by default. A tick that finds nothing moved is a
survey and no run, and one that finds a pass still out does nothing. The log
says when a scheduled pass starts and what it is reading, and nothing when it
had nothing to do. everyMinutes: 0 is a node that would rather be asked.
Asking is the same pass, with an answer:
pac api POST /flows/intents/extractanswers 202 {extracting, chunks, toReason, projects, inputs, intents} once it
has asked what moved and knows what it will cost, and leaves the runs going.
Asking again while a run is out is a 409; asking after a pass finished, with
nothing changed since, is a 200 {extracting: false} rather than a bill.
{"force": true} reads everything again and folds it into the list that stands
-- which is how a prompt change reaches a history that has not moved.
pac api GET /flows/intents/extractis how far it got: the pass state, every chunk's, which inputs contributed, and how many intents are open, done and dropped. Never a row.
What the schedule costs. The first pass over a real history is agent-history's ten runs plus one. After that a tick pays for exactly what moved, and the thing that moves most reliably is the transcript of the session the principal is in right now: while they work, an hourly tick is a propose run over that session and a revise, and while they do not, it is nothing. That is the bound an hour buys, and why the default is not a minute.
It needs a model, and it keeps its rows through @projectpac/records, so GET
/plugins says waitingFor: ["records"] until that is installed. The sources
it reads are not waited on: each contributes when it is there.
| field | default | why |
| ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- |
| inputs | agent-history and conversation-notes, as above | which sources to reason over, each by the service key it claims |
| maxChunkBytes | 640 KiB | a packing size, under agent-history's: every message carries an id and a time |
| maxConcurrentRuns | 2 | politeness toward every other flow's runs, not parallelism |
| maxChunkAttempts | 3 | a chunk that keeps failing is not retried by every pass forever |
| executor | cli | the executor row every run names; cli is what every setup writes |
| maxRevisedIntents | 200 | how many intents the revise run is shown: open first, newest first |
| everyMinutes | 60 | how often a pass runs on its own; 0 is only when asked |
What this writes down
More than the profile does, and it should be said plainly. The source carries verbatim excerpts of what the principal typed to their agent and of what the skill wrote about their conversations. The transcripts arrive through agent-history's redaction and with its tool output already dropped, which is where credentials actually live -- but anything that stages this source receives those excerpts. And the intelligence store keeps every run's prompt regardless of what this plugin writes: the material is never a source, but a chunk of it is a run's context, and that is the node's to keep.
Not yet
No route dismisses an intent, because no route lists them for a person to choose from; that surface comes with a face that can show ids. Two existing intents that turn out to be one cannot be merged, since leaving a row out keeps it. A claude.ai data export, or a Codex or Cursor transcript, is a reader in agent-history, and reaches this plugin the moment agent-history has it.
