local CMD_PERSONA_PACK = std.env.get_or("WAKE_CMD_PERSONA_PACK", "persona-pack-mcp")
local CMD_PERSONA_WORK = std.env.get_or("WAKE_CMD_PERSONA_WORK", "persona-work-mcp")
local CMD_MINI_APP = std.env.get_or("WAKE_CMD_MINI_APP", "mini-app-mcp")
local CMD_PERSONA_JOURNAL = std.env.get_or("WAKE_CMD_PERSONA_JOURNAL", "persona-journal-mcp")
local function extract_mcp_text(result)
if not result or not result.ok then
return nil
end
local blocks = result.content or {}
local parts = {}
for _, b in ipairs(blocks) do
if b.type == "text" and b.text and b.text ~= "" then
table.insert(parts, b.text)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "\n")
end
local function safe_connect(alias, command, args)
local ok, err = pcall(mcp.connect, alias, command, args or {})
if not ok then
return false, tostring(err)
end
return true, nil
end
local function safe_disconnect(alias)
local ok, err = pcall(mcp.disconnect, alias)
if not ok then
log.warn("wake_compile: disconnect error for '" .. alias .. "': " .. tostring(err))
end
end
local function apply_deny(text, patterns)
if not text or #patterns == 0 then
return text
end
local lines = {}
for line in (text .. "\n"):gmatch("([^\n]*)\n") do
local blocked = false
for _, pat in ipairs(patterns) do
if pat ~= "" and line:find(pat, 1, true) then
blocked = true
break
end
end
if not blocked then
table.insert(lines, line)
end
end
return table.concat(lines, "\n")
end
local function resolve_identity(persona_id)
local alias = "wake_pp_" .. persona_id
local ok, err = safe_connect(alias, CMD_PERSONA_PACK)
if not ok then
return nil, "source unavailable: " .. (err or "connect failed")
end
local result = mcp.call(alias, "persona_render", {
id = persona_id,
format = "prompt",
})
safe_disconnect(alias)
local text = extract_mcp_text(result)
if not text then
local reason = (result and result.error) or "empty response"
return nil, "resolve failed: " .. tostring(reason)
end
return text, nil
end
local function resolve_attention(persona_id)
local alias = "wake_pw_attn_" .. persona_id
local ok, err = safe_connect(alias, CMD_PERSONA_WORK)
if not ok then
return nil, "source unavailable: " .. (err or "connect failed")
end
local result = mcp.call(alias, "attention_list", { persona_id = persona_id })
safe_disconnect(alias)
local text = extract_mcp_text(result)
if not text then
local reason = (result and result.error) or "empty response"
return nil, "resolve failed: " .. tostring(reason)
end
return text, nil
end
local function resolve_schedule(persona_id)
local alias = "wake_pw_sched_" .. persona_id
local ok, err = safe_connect(alias, CMD_PERSONA_WORK)
if not ok then
return nil, "source unavailable: " .. (err or "connect failed")
end
local result = mcp.call(alias, "schedule_due_scan", { owner = persona_id })
safe_disconnect(alias)
local text = extract_mcp_text(result)
if not text then
local reason = (result and result.error) or "empty response"
return nil, "resolve failed: " .. tostring(reason)
end
return text, nil
end
local function resolve_mailbox(persona_id)
local alias = "wake_ma_mb_" .. persona_id
local ok, err = safe_connect(alias, CMD_MINI_APP, { "--mcp" })
if not ok then
return nil, "source unavailable: " .. (err or "connect failed")
end
local result = mcp.call(alias, "list", {
table = "mailbox",
filter = {
type = "or",
filters = {
{ type = "eq", field = "to", value = persona_id },
{ type = "eq", field = "to", value = "*" },
},
},
limit = 50,
})
safe_disconnect(alias)
if not result or not result.ok then
local reason = (result and result.error) or "call failed"
return nil, "resolve failed: " .. tostring(reason)
end
local content_raw = result.content or {}
local raw_text = nil
for _, b in ipairs(content_raw) do
if b.type == "text" and b.text then
raw_text = b.text
break
end
end
if not raw_text or raw_text == "" then
return nil, "empty mailbox"
end
local items_ok, items = pcall(std.json.decode, raw_text)
if not items_ok or type(items) ~= "table" then
return raw_text, nil
end
local lines = {}
for _, row in ipairs(items) do
local d = row.data or {}
local already_read = false
for _, reader in ipairs(d.read_by or {}) do
if reader == persona_id then
already_read = true
break
end
end
if not already_read then
local subject = d.subject or "(件名なし)"
local from = d.from or "?"
table.insert(lines, "* [" .. from .. "] " .. subject)
end
end
if #lines == 0 then
return nil, "no unread mail"
end
return table.concat(lines, "\n"), nil
end
local function resolve_journal(persona_id)
local alias = "wake_pj_" .. persona_id
local ok, err = safe_connect(alias, CMD_PERSONA_JOURNAL)
if not ok then
return nil, "source unavailable: " .. (err or "connect failed")
end
local result = mcp.call(alias, "journal_query_latest", {
persona = persona_id,
kind = "states,emo",
count = 1,
})
safe_disconnect(alias)
local text = extract_mcp_text(result)
if not text then
local reason = (result and result.error) or "empty response"
return nil, "resolve failed: " .. tostring(reason)
end
return text, nil
end
local function resolve_awareness(persona_id)
local alias = "wake_pw_aware_" .. persona_id
local ok, err = safe_connect(alias, CMD_PERSONA_WORK)
if not ok then
return nil, "source unavailable: " .. (err or "connect failed")
end
local result = mcp.call(alias, "awareness_list", { persona_id = persona_id })
safe_disconnect(alias)
local text = extract_mcp_text(result)
if not text then
local reason = (result and result.error) or "empty response"
return nil, "resolve failed: " .. tostring(reason)
end
return text, nil
end
local function fetch_deny_patterns(persona_id)
local alias = "wake_pw_deny_" .. persona_id
local ok, _ = safe_connect(alias, CMD_PERSONA_WORK)
if not ok then
log.warn("wake_compile: deny list unavailable, skip deny filter")
return {}
end
local result = mcp.call(alias, "deny_list", { persona_id = persona_id })
safe_disconnect(alias)
if not result or not result.ok then
log.warn("wake_compile: deny_list call failed, skip deny filter")
return {}
end
local text = extract_mcp_text(result)
if not text or text == "" then
return {}
end
local patterns = {}
for line in (text .. "\n"):gmatch("([^\n]+)\n") do
local trimmed = line:match("^%s*(.-)%s*$")
if trimmed ~= "" then
table.insert(patterns, trimmed)
end
end
return patterns
end
local RESOLVERS = {
identity = resolve_identity,
attention = resolve_attention,
schedule = resolve_schedule,
mailbox = resolve_mailbox,
journal = resolve_journal,
awareness = resolve_awareness,
}
local function compile(policy, persona_id)
local deny_patterns = {}
if policy.deny then
deny_patterns = fetch_deny_patterns(persona_id)
log.info("wake_compile: deny patterns loaded: " .. #deny_patterns)
end
local sections = {}
for _, s in ipairs(policy.sections) do
table.insert(sections, s)
end
table.sort(sections, function(a, b)
return (a.priority or 99) < (b.priority or 99)
end)
local prompt_parts = {}
local trace = {}
local used_chars = 0
local budget = policy.budget or 4000
for _, sec in ipairs(sections) do
local feed = sec.feed
local max_ch = sec.max_chars or 500
local resolver = RESOLVERS[feed]
if not resolver then
table.insert(trace, {
feed = feed,
status = "skipped",
chars = 0,
reason = "no resolver defined for src=" .. (sec.src or "?"),
})
goto continue
end
if used_chars >= budget then
table.insert(trace, {
feed = feed,
status = "dropped",
chars = 0,
reason = "global budget exhausted (" .. used_chars .. "/" .. budget .. ")",
})
goto continue
end
local text, resolve_err = resolver(persona_id)
if not text then
table.insert(trace, {
feed = feed,
status = "dropped",
chars = 0,
reason = resolve_err or "resolve returned nil",
})
goto continue
end
text = apply_deny(text, deny_patterns)
if #text > max_ch then
table.insert(trace, {
feed = feed,
status = "dropped",
chars = #text,
reason = "section budget exceeded (" .. #text .. " > " .. max_ch .. ")",
})
goto continue
end
if used_chars + #text > budget then
table.insert(trace, {
feed = feed,
status = "dropped",
chars = #text,
reason = "global budget would be exceeded (used="
.. used_chars
.. " + this="
.. #text
.. " > "
.. budget
.. ")",
})
goto continue
end
table.insert(prompt_parts, "## " .. feed .. "\n" .. text)
used_chars = used_chars + #text
table.insert(trace, {
feed = feed,
status = "included",
chars = #text,
reason = nil,
})
::continue::
end
local compiled = table.concat(prompt_parts, "\n\n")
return compiled, trace
end
local persona_id = (_PROMPT and _PROMPT ~= "" and _PROMPT) or std.env.get_or("WAKE_PERSONA", "shi")
persona_id = persona_id:match("^%s*(.-)%s*$")
log.info("wake_compile: persona_id=" .. persona_id)
local policy_module = "wake_compile.policy." .. persona_id
local policy_ok, policy = pcall(require, policy_module)
if not policy_ok then
log.warn("wake_compile: no policy for persona '" .. persona_id .. "', using empty policy")
policy = { budget = 4000, sections = {}, deny = nil }
end
local compiled, trace = compile(policy, persona_id)
print(compiled)
print("")
print("--- COMPILE TRACE ---")
for _, t in ipairs(trace) do
local line = t.feed .. ": " .. t.status
if t.chars and t.chars > 0 then
line = line .. ", " .. t.chars .. " chars"
end
if t.reason then
line = line .. ", reason=" .. t.reason
end
print(line)
end