niahere 0.2.81 → 0.2.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/chat/engine.ts +8 -1
- package/src/cli/job.ts +17 -2
- package/src/core/job-prompt.ts +78 -0
- package/src/core/runner.ts +6 -43
- package/src/db/models/job.ts +2 -0
- package/src/mcp/server.ts +11 -2
- package/src/mcp/tools.ts +20 -1
- package/src/prompts/environment.md +4 -2
- package/src/types/index.ts +1 -1
- package/src/types/job.ts +8 -0
- package/src/utils/job-workspace.ts +19 -0
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ All config and data lives in `~/.niahere/`:
|
|
|
110
110
|
soul.md — how the agent works
|
|
111
111
|
rules.md — behavioral instructions (loaded every session)
|
|
112
112
|
memory.md — persistent facts and context (loaded every session)
|
|
113
|
-
jobs/ — per-job working memory and state (auto-created)
|
|
113
|
+
jobs/ — per-job prompt.md, working memory, and state (auto-created)
|
|
114
114
|
optimizations/ — optimization loop run workspaces
|
|
115
115
|
images/
|
|
116
116
|
reference.webp — visual identity reference image
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niahere",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.83",
|
|
4
4
|
"description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"private": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
47
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.126",
|
|
48
48
|
"@anthropic-ai/sdk": "^0.88.0",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
50
50
|
"@slack/bolt": "^4.6.0",
|
package/src/chat/engine.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { finalizeSession, cancelPending } from "../core/finalizer";
|
|
|
24
24
|
import { log } from "../utils/log";
|
|
25
25
|
import { isRetryableApiError, sleep } from "../utils/retry";
|
|
26
26
|
import { registerActiveHandle, unregisterActiveHandle } from "../core/active-handles";
|
|
27
|
+
import { resolveJobPrompt } from "../core/job-prompt";
|
|
27
28
|
|
|
28
29
|
const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
29
30
|
const LONG_RUNNING_WARN = 30 * 60 * 1000; // 30 minutes
|
|
@@ -101,6 +102,9 @@ export function formatChatError(rawError: string | null | undefined): string {
|
|
|
101
102
|
if (!error || error.toLowerCase() === "unknown error") {
|
|
102
103
|
return GENERIC_CHAT_ERROR;
|
|
103
104
|
}
|
|
105
|
+
if (error === "oauth_org_not_allowed") {
|
|
106
|
+
return "[error] This Claude account is not allowed to access the configured organization. Check your Claude login or organization access.";
|
|
107
|
+
}
|
|
104
108
|
return `[error] ${error}`;
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -198,7 +202,9 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
198
202
|
const agentDef = agents.find((a) => a.name === jobData.agent);
|
|
199
203
|
if (agentDef) systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
|
|
200
204
|
}
|
|
201
|
-
|
|
205
|
+
const resolvedPrompt = resolveJobPrompt(jobData);
|
|
206
|
+
const source = resolvedPrompt.source === "file" ? ` from ${resolvedPrompt.filePath}` : "";
|
|
207
|
+
systemPrompt += `\n\n## Job Context\nYou are chatting in the context of job "${jobData.name}" (schedule: ${jobData.schedule}).\n\nJob prompt (${resolvedPrompt.source}${source}):\n${resolvedPrompt.prompt}`;
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -308,6 +314,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
308
314
|
permissionMode: "bypassPermissions",
|
|
309
315
|
includePartialMessages: true,
|
|
310
316
|
settingSources: ["project", "user"],
|
|
317
|
+
skills: [],
|
|
311
318
|
};
|
|
312
319
|
|
|
313
320
|
if (sessionId) {
|
package/src/cli/job.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { ScheduleType } from "../types";
|
|
|
10
10
|
import { errMsg } from "../utils/errors";
|
|
11
11
|
import { fail, parseArgs, pickFromList, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
12
12
|
import { computeInitialNextRun } from "../core/scheduler";
|
|
13
|
+
import { resolveJobPrompt } from "../core/job-prompt";
|
|
13
14
|
|
|
14
15
|
const HELP = `Usage: nia job <command>
|
|
15
16
|
|
|
@@ -104,7 +105,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
104
105
|
const type = job.scheduleType !== "cron" ? ` (${job.scheduleType})` : "";
|
|
105
106
|
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
106
107
|
const empTag = job.employee ? ` [emp:${job.employee}]` : "";
|
|
107
|
-
|
|
108
|
+
const promptTag = resolveJobPrompt(job).source === "file" ? " [prompt.md]" : "";
|
|
109
|
+
console.log(` ${icon} ${job.name} ${job.schedule}${type}${tag}${agentTag}${empTag}${promptTag}`);
|
|
108
110
|
}
|
|
109
111
|
if (archived.length > 0) {
|
|
110
112
|
console.log(
|
|
@@ -283,6 +285,15 @@ export async function jobCommand(): Promise<void> {
|
|
|
283
285
|
const updated = await Job.update(name, fields);
|
|
284
286
|
if (!updated) fail(`Job not found: "${name}". Use \`nia job list\` to see available jobs.`);
|
|
285
287
|
console.log(`Job "${name}" updated.`);
|
|
288
|
+
if (fields.prompt !== undefined) {
|
|
289
|
+
const job = await Job.get(name);
|
|
290
|
+
if (job) {
|
|
291
|
+
const resolvedPrompt = resolveJobPrompt(job);
|
|
292
|
+
if (resolvedPrompt.source === "file") {
|
|
293
|
+
console.log(`Note: runtime prompt is still overridden by ${resolvedPrompt.filePath}.`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
286
297
|
});
|
|
287
298
|
} catch (err) {
|
|
288
299
|
fail(`Failed to update job: ${errMsg(err)}`);
|
|
@@ -307,7 +318,11 @@ export async function jobCommand(): Promise<void> {
|
|
|
307
318
|
if (job.employee) console.log(` employee: ${job.employee}`);
|
|
308
319
|
if (job.model) console.log(` model: ${job.model}`);
|
|
309
320
|
if (job.stateless) console.log(` stateless: true`);
|
|
310
|
-
|
|
321
|
+
const resolvedPrompt = resolveJobPrompt(job);
|
|
322
|
+
const promptSource =
|
|
323
|
+
resolvedPrompt.source === "file" ? `file (${resolvedPrompt.filePath})` : resolvedPrompt.source;
|
|
324
|
+
console.log(` prompt source: ${promptSource}`);
|
|
325
|
+
console.log(` prompt: ${resolvedPrompt.prompt}`);
|
|
311
326
|
|
|
312
327
|
const state = readState();
|
|
313
328
|
const info = state[job.name];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { JobInput, ResolvedJobPrompt } from "../types";
|
|
4
|
+
import { log } from "../utils/log";
|
|
5
|
+
import { getJobDir } from "../utils/job-workspace";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_JOB_PROMPT = "Execute your scheduled tasks.";
|
|
8
|
+
|
|
9
|
+
export { getJobDir } from "../utils/job-workspace";
|
|
10
|
+
|
|
11
|
+
export function resolveJobPrompt(job: JobInput): ResolvedJobPrompt {
|
|
12
|
+
let promptPath: string | null = null;
|
|
13
|
+
try {
|
|
14
|
+
promptPath = join(getJobDir(job.name), "prompt.md");
|
|
15
|
+
} catch (err) {
|
|
16
|
+
log.warn({ err, job: job.name }, "job has no safe workspace; falling back to database prompt");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (promptPath && existsSync(promptPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const filePrompt = readFileSync(promptPath, "utf8").trim();
|
|
22
|
+
if (filePrompt) {
|
|
23
|
+
return { prompt: filePrompt, source: "file", filePath: promptPath };
|
|
24
|
+
}
|
|
25
|
+
log.warn({ job: job.name, promptPath }, "job prompt.md is empty; falling back to database prompt");
|
|
26
|
+
} catch (err) {
|
|
27
|
+
log.warn({ err, job: job.name, promptPath }, "failed to read job prompt.md; falling back to database prompt");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const dbPrompt = job.prompt.trim();
|
|
32
|
+
if (dbPrompt) {
|
|
33
|
+
return { prompt: dbPrompt, source: "database", filePath: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { prompt: DEFAULT_JOB_PROMPT, source: "default", filePath: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build the working memory block for a stateful job. Returns empty string for stateless jobs. */
|
|
40
|
+
export function buildWorkingMemory(jobName: string, stateless?: boolean): string {
|
|
41
|
+
if (stateless) return "";
|
|
42
|
+
|
|
43
|
+
let jobDir: string;
|
|
44
|
+
try {
|
|
45
|
+
jobDir = getJobDir(jobName);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.warn({ err, job: jobName }, "job has no safe workspace; working memory disabled");
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
mkdirSync(jobDir, { recursive: true });
|
|
51
|
+
const statePath = join(jobDir, "state.md");
|
|
52
|
+
let stateContent = "";
|
|
53
|
+
if (existsSync(statePath)) {
|
|
54
|
+
try {
|
|
55
|
+
stateContent = readFileSync(statePath, "utf8").trim();
|
|
56
|
+
} catch {
|
|
57
|
+
stateContent = "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const stateBlock = stateContent ? `\n${stateContent}\n` : "(first run - no prior state)";
|
|
62
|
+
|
|
63
|
+
return `
|
|
64
|
+
|
|
65
|
+
## Working Memory
|
|
66
|
+
|
|
67
|
+
You have a persistent workspace at \`${jobDir}/\`. This directory is yours - create files, organize data, track history, maintain state however you need.
|
|
68
|
+
|
|
69
|
+
Your \`state.md\` from last run:
|
|
70
|
+
${stateBlock}
|
|
71
|
+
|
|
72
|
+
Before finishing, update \`state.md\` with: what you did this run, what you noticed, and what to do or focus on next time. Keep it concise - a working notebook, not a log.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildJobPrompt(job: JobInput): string {
|
|
76
|
+
const resolved = resolveJobPrompt(job);
|
|
77
|
+
return `Job: ${job.name} (schedule: ${job.schedule})\n\n${resolved.prompt}${buildWorkingMemory(job.name, job.stateless)}`;
|
|
78
|
+
}
|
package/src/core/runner.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
|
-
import { existsSync
|
|
3
|
-
import { join } from "path";
|
|
2
|
+
import { existsSync } from "fs";
|
|
4
3
|
import { randomUUID } from "crypto";
|
|
5
4
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
6
5
|
import type { JobInput, JobResult } from "../types";
|
|
@@ -11,14 +10,16 @@ import { buildSystemPrompt, buildContextSuffix } from "../chat/identity";
|
|
|
11
10
|
import { buildEmployeePrompt } from "../chat/employee-prompt";
|
|
12
11
|
import { getEmployee } from "./employees";
|
|
13
12
|
import { scanAgents } from "./agents";
|
|
13
|
+
import { buildJobPrompt } from "./job-prompt";
|
|
14
14
|
import { truncate, formatToolUse } from "../utils/format-activity";
|
|
15
15
|
import { getMcpServers, type McpSourceContext } from "../mcp";
|
|
16
16
|
import { ActiveEngine } from "../db/models";
|
|
17
|
-
import { getPaths } from "../utils/paths";
|
|
18
17
|
import { log } from "../utils/log";
|
|
19
18
|
import { isRetryableApiError, sleep } from "../utils/retry";
|
|
20
19
|
import { registerActiveHandle, unregisterActiveHandle } from "./active-handles";
|
|
21
20
|
|
|
21
|
+
export { buildWorkingMemory } from "./job-prompt";
|
|
22
|
+
|
|
22
23
|
export type ActivityCallback = (line: string) => void;
|
|
23
24
|
|
|
24
25
|
interface RunnerOutput {
|
|
@@ -118,6 +119,7 @@ export async function runJobWithClaude(
|
|
|
118
119
|
cwd,
|
|
119
120
|
permissionMode: "bypassPermissions",
|
|
120
121
|
sessionId,
|
|
122
|
+
skills: [],
|
|
121
123
|
};
|
|
122
124
|
|
|
123
125
|
if (model && model !== "default") {
|
|
@@ -284,40 +286,6 @@ export async function runTask(opts: TaskOptions): Promise<RunnerOutput> {
|
|
|
284
286
|
}
|
|
285
287
|
}
|
|
286
288
|
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
// Working memory
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
/** Build the working memory block for a stateful job. Returns empty string for stateless jobs. */
|
|
292
|
-
export function buildWorkingMemory(jobName: string, stateless?: boolean): string {
|
|
293
|
-
if (stateless) return "";
|
|
294
|
-
|
|
295
|
-
const jobDir = join(getPaths().jobsDir, jobName);
|
|
296
|
-
mkdirSync(jobDir, { recursive: true });
|
|
297
|
-
const statePath = join(jobDir, "state.md");
|
|
298
|
-
let stateContent = "";
|
|
299
|
-
if (existsSync(statePath)) {
|
|
300
|
-
try {
|
|
301
|
-
stateContent = readFileSync(statePath, "utf8").trim();
|
|
302
|
-
} catch {
|
|
303
|
-
stateContent = "";
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const stateBlock = stateContent ? `\n${stateContent}\n` : "(first run — no prior state)";
|
|
308
|
-
|
|
309
|
-
return `
|
|
310
|
-
|
|
311
|
-
## Working Memory
|
|
312
|
-
|
|
313
|
-
You have a persistent workspace at \`${jobDir}/\`. This directory is yours — create files, organize data, track history, maintain state however you need.
|
|
314
|
-
|
|
315
|
-
Your \`state.md\` from last run:
|
|
316
|
-
${stateBlock}
|
|
317
|
-
|
|
318
|
-
Before finishing, update \`state.md\` with: what you did this run, what you noticed, and what to do or focus on next time. Keep it concise — a working notebook, not a log.`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
289
|
// ---------------------------------------------------------------------------
|
|
322
290
|
// Public API
|
|
323
291
|
// ---------------------------------------------------------------------------
|
|
@@ -364,12 +332,7 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
364
332
|
systemPrompt = buildSystemPrompt("job");
|
|
365
333
|
}
|
|
366
334
|
|
|
367
|
-
|
|
368
|
-
? `Job: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`
|
|
369
|
-
: `Job: ${job.name} (schedule: ${job.schedule})\n\nExecute your scheduled tasks.`;
|
|
370
|
-
|
|
371
|
-
// Working memory: give stateful jobs a persistent workspace
|
|
372
|
-
jobPrompt += buildWorkingMemory(job.name, job.stateless);
|
|
335
|
+
const jobPrompt = buildJobPrompt(job);
|
|
373
336
|
|
|
374
337
|
// Model priority: job.model > agent.model > config.model
|
|
375
338
|
const resolvedModel = job.model || agentModel || config.model;
|
package/src/db/models/job.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CronExpressionParser } from "cron-parser";
|
|
|
3
3
|
import { parseDuration } from "../../utils/duration";
|
|
4
4
|
import { computeInitialNextRun } from "../../utils/schedule";
|
|
5
5
|
import { getConfig } from "../../utils/config";
|
|
6
|
+
import { validateJobName } from "../../utils/job-workspace";
|
|
6
7
|
import type { ScheduleType, JobLifecycle } from "../../types";
|
|
7
8
|
|
|
8
9
|
/** Validate that a schedule string matches its declared type. Throws on mismatch. */
|
|
@@ -96,6 +97,7 @@ export async function create(
|
|
|
96
97
|
model?: string,
|
|
97
98
|
employee?: string,
|
|
98
99
|
): Promise<void> {
|
|
100
|
+
validateJobName(name);
|
|
99
101
|
validateSchedule(schedule, scheduleType);
|
|
100
102
|
const existing = await get(name);
|
|
101
103
|
if (existing) {
|
package/src/mcp/server.ts
CHANGED
|
@@ -17,7 +17,11 @@ export function createNiaMcpServer(sourceCtx?: McpSourceContext) {
|
|
|
17
17
|
{
|
|
18
18
|
name: z.string().describe("Unique job name"),
|
|
19
19
|
schedule: z.string().describe("Cron expression, duration string, or ISO timestamp"),
|
|
20
|
-
prompt: z
|
|
20
|
+
prompt: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe(
|
|
23
|
+
"What the job should do. A non-empty ~/.niahere/jobs/<job-name>/prompt.md overrides this database prompt at runtime.",
|
|
24
|
+
),
|
|
21
25
|
schedule_type: z.enum(["cron", "interval", "once"]).default("cron").describe("Schedule type"),
|
|
22
26
|
always: z.boolean().default(false).describe("If true, runs 24/7 ignoring active hours"),
|
|
23
27
|
agent: z
|
|
@@ -50,7 +54,12 @@ export function createNiaMcpServer(sourceCtx?: McpSourceContext) {
|
|
|
50
54
|
.string()
|
|
51
55
|
.optional()
|
|
52
56
|
.describe("New schedule (cron expression, interval duration, or ISO timestamp)"),
|
|
53
|
-
prompt: z
|
|
57
|
+
prompt: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe(
|
|
61
|
+
"New database prompt. A non-empty ~/.niahere/jobs/<job-name>/prompt.md overrides this at runtime.",
|
|
62
|
+
),
|
|
54
63
|
always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
|
|
55
64
|
agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
|
|
56
65
|
employee: z.string().nullable().optional().describe("Employee name (set null to remove employee)"),
|
package/src/mcp/tools.ts
CHANGED
|
@@ -11,12 +11,22 @@ import { log } from "../utils/log";
|
|
|
11
11
|
import { classifyMime } from "../utils/attachment";
|
|
12
12
|
import { scanAgents } from "../core/agents";
|
|
13
13
|
import { listEmployeesForMcp } from "../core/employees";
|
|
14
|
+
import { resolveJobPrompt } from "../core/job-prompt";
|
|
14
15
|
import type { McpSourceContext } from "./index";
|
|
15
16
|
|
|
16
17
|
export async function listJobs(): Promise<string> {
|
|
17
18
|
const jobs = await Job.list();
|
|
18
19
|
if (jobs.length === 0) return "No jobs found.";
|
|
19
|
-
|
|
20
|
+
const withPromptSource = jobs.map((job) => {
|
|
21
|
+
const resolvedPrompt = resolveJobPrompt(job);
|
|
22
|
+
return {
|
|
23
|
+
...job,
|
|
24
|
+
prompt: resolvedPrompt.prompt,
|
|
25
|
+
promptSource: resolvedPrompt.source,
|
|
26
|
+
promptPath: resolvedPrompt.filePath,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
return JSON.stringify(withPromptSource, null, 2);
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export async function addJob(args: {
|
|
@@ -89,6 +99,15 @@ export async function updateJob(args: {
|
|
|
89
99
|
|
|
90
100
|
const updated = await Job.update(args.name, fields);
|
|
91
101
|
if (!updated) return `Job "${args.name}" not found.`;
|
|
102
|
+
if (fields.prompt !== undefined) {
|
|
103
|
+
const job = await Job.get(args.name);
|
|
104
|
+
if (job) {
|
|
105
|
+
const resolvedPrompt = resolveJobPrompt(job);
|
|
106
|
+
if (resolvedPrompt.source === "file") {
|
|
107
|
+
return `Job "${args.name}" updated. Note: runtime prompt is still overridden by ${resolvedPrompt.filePath}.`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
92
111
|
return `Job "${args.name}" updated.`;
|
|
93
112
|
}
|
|
94
113
|
|
|
@@ -39,7 +39,7 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
|
|
|
39
39
|
- Set `stateless: true` to disable working memory (no state.md or workspace)
|
|
40
40
|
- Set `model` to override the default (e.g., `haiku`, `sonnet`, `opus`) — use cheaper models for high-frequency or simple jobs. Priority: job model > agent model > config model.
|
|
41
41
|
- Set `employee` to assign the job to an employee (employee identity takes precedence over agent)
|
|
42
|
-
- **update_job** — update an existing job's schedule, prompt, always, stateless, agent, model, or employee
|
|
42
|
+
- **update_job** — update an existing job's schedule, prompt, always, stateless, agent, model, or employee. If `~/.niahere/jobs/<job-name>/prompt.md` exists and is non-empty, it overrides the database prompt at runtime.
|
|
43
43
|
- **remove_job** — delete a job by name
|
|
44
44
|
- **enable_job** / **disable_job** — toggle a job on or off
|
|
45
45
|
- **archive_job** — archive a job (hidden from list, won't run)
|
|
@@ -68,7 +68,9 @@ Active hours: {{activeStart}}–{{activeEnd}} ({{timezone}}). Jobs respect this;
|
|
|
68
68
|
|
|
69
69
|
Jobs are **stateful by default**. Each job gets a persistent workspace at `~/.niahere/jobs/<job-name>/`. Before each run, the runner reads `state.md` from that directory and injects it into the prompt. The agent should update `state.md` at the end of each run with what it did, what it noticed, and what to focus on next time.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Jobs may also keep their task instructions in `~/.niahere/jobs/<job-name>/prompt.md`. If this file exists and is non-empty, it is the runtime job prompt. If it is missing or empty, the job falls back to the database prompt set through `add_job`, `update_job`, or `nia job update --prompt`.
|
|
72
|
+
|
|
73
|
+
The workspace is freeform — the agent can create any files it needs (data, cache, history, etc.). `prompt.md` is the editable task prompt convention; `state.md` is the working-memory convention the runner injects automatically.
|
|
72
74
|
|
|
73
75
|
To disable working memory for a specific job, set `stateless: true` when creating or updating it.
|
|
74
76
|
|
package/src/types/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { Attachment } from "./attachment";
|
|
2
2
|
export type { JobStatus, JobStateStatus, JobLifecycle, ScheduleType, Mode, AttachmentType, ChannelName } from "./enums";
|
|
3
|
-
export type { JobInput, JobResult } from "./job";
|
|
3
|
+
export type { JobInput, JobPromptSource, JobResult, ResolvedJobPrompt } from "./job";
|
|
4
4
|
export type { SendResult, StreamCallback, ActivityCallback, SendCallbacks, ChatEngine, EngineOptions } from "./engine";
|
|
5
5
|
export type { AuditEntry, JobState, CronState } from "./audit";
|
|
6
6
|
export type { Channel, ChannelFactory } from "./channel";
|
package/src/types/job.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface JobInput {
|
|
|
10
10
|
stateless?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type JobPromptSource = "file" | "database" | "default";
|
|
14
|
+
|
|
15
|
+
export interface ResolvedJobPrompt {
|
|
16
|
+
prompt: string;
|
|
17
|
+
source: JobPromptSource;
|
|
18
|
+
filePath: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export interface JobResult {
|
|
14
22
|
job: string;
|
|
15
23
|
timestamp: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
2
|
+
|
|
3
|
+
import { getPaths } from "./paths";
|
|
4
|
+
|
|
5
|
+
export function getJobDir(jobName: string): string {
|
|
6
|
+
const jobsDir = resolve(getPaths().jobsDir);
|
|
7
|
+
const jobDir = resolve(jobsDir, jobName);
|
|
8
|
+
const rel = relative(jobsDir, jobDir);
|
|
9
|
+
|
|
10
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
11
|
+
throw new Error(`Invalid job name "${jobName}": job workspace must stay inside ${jobsDir}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return jobDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateJobName(jobName: string): void {
|
|
18
|
+
getJobDir(jobName);
|
|
19
|
+
}
|