mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(tinyplace): autonomous tiny.place agent — scheduled bounty runs (opt-in) (#3925)
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
type TrustedAccess,
|
||||
type TrustedRoot,
|
||||
} from '../../../utils/tauriCommands';
|
||||
import { openhumanCronList, openhumanCronUpdate } from '../../../utils/tauriCommands/cron';
|
||||
import PanelPage from '../../layout/PanelPage';
|
||||
import Button from '../../ui/Button';
|
||||
import SettingsBackButton from '../components/SettingsBackButton';
|
||||
@@ -54,6 +55,16 @@ const AgentAccessPanel = () => {
|
||||
const [newRootPath, setNewRootPath] = useState('');
|
||||
const [newRootAccess, setNewRootAccess] = useState<TrustedAccess>('read');
|
||||
|
||||
// Autonomous tiny.place agent ("autopilot") — a seeded, *disabled* cron job
|
||||
// the user opts into here. It's not an autonomy field: we resolve its id by
|
||||
// name from the cron list and flip its `enabled` flag via cron_update. The
|
||||
// section only renders once the job is found (id known).
|
||||
const [autopilotJobId, setAutopilotJobId] = useState<string | null>(null);
|
||||
const [autopilotEnabled, setAutopilotEnabled] = useState(false);
|
||||
// Monotonic guard so rapid toggles can't resolve out-of-order and leave the
|
||||
// UI showing a stale enabled state (last write wins).
|
||||
const autopilotSeqRef = useRef(0);
|
||||
|
||||
// Action timeout (the tool/action wall-clock limit, issue #3100). Held as the
|
||||
// raw input string so the field can be edited freely; validated on save.
|
||||
const [timeoutInput, setTimeoutInput] = useState('');
|
||||
@@ -93,6 +104,20 @@ const AgentAccessPanel = () => {
|
||||
if (!cancelled)
|
||||
setError(e instanceof Error ? e.message : t('settings.agentAccess.loadError'));
|
||||
}
|
||||
try {
|
||||
// Resolve the seeded tinyplace_autopilot cron job by name so the toggle
|
||||
// below can flip its enabled flag. Non-fatal: the section just stays
|
||||
// hidden if the job isn't present or the list call fails.
|
||||
const cronResp = await openhumanCronList();
|
||||
if (cancelled) return;
|
||||
const autopilot = cronResp.result.find(j => j.name === 'tinyplace_autopilot');
|
||||
if (autopilot) {
|
||||
setAutopilotJobId(autopilot.id);
|
||||
setAutopilotEnabled(autopilot.enabled);
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal — bounty-worker toggle stays hidden.
|
||||
}
|
||||
try {
|
||||
const agentResp = await openhumanGetAgentSettings();
|
||||
if (cancelled) return;
|
||||
@@ -168,6 +193,29 @@ const AgentAccessPanel = () => {
|
||||
void persist({ workspaceOnly, requireTaskPlanApproval: next, trustedRoots });
|
||||
};
|
||||
|
||||
// The autopilot is a cron job, not an autonomy field — flip its `enabled`
|
||||
// flag directly via cron_update. Optimistic, with revert on failure, and a
|
||||
// sequence guard so only the most recent toggle writes UI state back.
|
||||
const toggleAutopilot = async (next: boolean) => {
|
||||
if (!autopilotJobId || !isTauri()) return;
|
||||
const seq = ++autopilotSeqRef.current;
|
||||
const prev = autopilotEnabled;
|
||||
setAutopilotEnabled(next);
|
||||
setError(null);
|
||||
setSavedNote(null);
|
||||
try {
|
||||
await openhumanCronUpdate(autopilotJobId, { enabled: next });
|
||||
if (autopilotSeqRef.current === seq) {
|
||||
setSavedNote(t('settings.agentAccess.saved'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (autopilotSeqRef.current === seq) {
|
||||
setAutopilotEnabled(prev);
|
||||
setError(e instanceof Error ? e.message : t('settings.agentAccess.saveError'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addRoot = () => {
|
||||
const path = newRootPath.trim();
|
||||
if (!path) return;
|
||||
@@ -284,6 +332,27 @@ const AgentAccessPanel = () => {
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
{/* Autonomous tiny.place agent (opt-in). Only shown once the seeded
|
||||
cron job is found, so users without it never see a dead toggle. */}
|
||||
{autopilotJobId && (
|
||||
<SettingsSection
|
||||
title={t('settings.agentAccess.tinyplaceAutopilot.title')}
|
||||
description={t('settings.agentAccess.tinyplaceAutopilot.desc')}>
|
||||
<SettingsRow
|
||||
htmlFor="switch-tinyplace-autopilot"
|
||||
label={t('settings.agentAccess.tinyplaceAutopilot.label')}
|
||||
control={
|
||||
<SettingsSwitch
|
||||
id="switch-tinyplace-autopilot"
|
||||
checked={autopilotEnabled}
|
||||
onCheckedChange={next => void toggleAutopilot(next)}
|
||||
aria-label={t('settings.agentAccess.tinyplaceAutopilot.label')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Action timeout */}
|
||||
<SettingsSection
|
||||
title={t('settings.agentAccess.timeout.label')}
|
||||
|
||||
@@ -11,6 +11,11 @@ import {
|
||||
openhumanUpdateAgentSettings,
|
||||
openhumanUpdateAutonomySettings,
|
||||
} from '../../../../utils/tauriCommands';
|
||||
import {
|
||||
type CoreCronJob,
|
||||
openhumanCronList,
|
||||
openhumanCronUpdate,
|
||||
} from '../../../../utils/tauriCommands/cron';
|
||||
import AgentAccessPanel from '../AgentAccessPanel';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -66,10 +71,35 @@ vi.mock('../../../../utils/tauriCommands', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../utils/tauriCommands/cron', () => ({
|
||||
openhumanCronList: vi.fn(),
|
||||
openhumanCronUpdate: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockGet = vi.mocked(openhumanGetAutonomySettings);
|
||||
const mockUpdate = vi.mocked(openhumanUpdateAutonomySettings);
|
||||
const mockGetAgent = vi.mocked(openhumanGetAgentSettings);
|
||||
const mockUpdateAgent = vi.mocked(openhumanUpdateAgentSettings);
|
||||
const mockCronList = vi.mocked(openhumanCronList);
|
||||
const mockCronUpdate = vi.mocked(openhumanCronUpdate);
|
||||
|
||||
// Minimal CoreCronJob for the seeded, disabled tinyplace_autopilot job.
|
||||
const autopilotJob = (overrides: Partial<CoreCronJob> = {}): CoreCronJob =>
|
||||
({
|
||||
id: 'tp-1',
|
||||
name: 'tinyplace_autopilot',
|
||||
enabled: false,
|
||||
expression: '',
|
||||
schedule: { kind: 'every', every_ms: 3600000 } as never,
|
||||
command: '',
|
||||
job_type: 'agent',
|
||||
session_target: 'isolated',
|
||||
delivery: { mode: 'proactive', best_effort: true },
|
||||
delete_after_run: false,
|
||||
created_at: '',
|
||||
next_run: '',
|
||||
...overrides,
|
||||
}) as CoreCronJob;
|
||||
|
||||
describe('AgentAccessPanel (advanced)', () => {
|
||||
beforeEach(() => {
|
||||
@@ -79,6 +109,8 @@ describe('AgentAccessPanel (advanced)', () => {
|
||||
mockUpdate.mockResolvedValue({ result: {} as never, logs: [] });
|
||||
mockGetAgent.mockResolvedValue({ result: agentSettings(), logs: [] });
|
||||
mockUpdateAgent.mockResolvedValue({ result: {} as never, logs: [] });
|
||||
mockCronList.mockResolvedValue({ result: [autopilotJob()], logs: [] });
|
||||
mockCronUpdate.mockResolvedValue({ result: autopilotJob({ enabled: true }), logs: [] });
|
||||
});
|
||||
|
||||
it('loads settings on mount and renders the advanced controls', async () => {
|
||||
@@ -115,6 +147,38 @@ describe('AgentAccessPanel (advanced)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the autopilot toggle when the seeded job is present', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await waitFor(() => expect(mockCronList).toHaveBeenCalledTimes(1));
|
||||
const sw = await screen.findByRole('switch', { name: /run automatically/i });
|
||||
expect(sw).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
it('enabling the autopilot flips its cron job enabled flag', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
const sw = await screen.findByRole('switch', { name: /run automatically/i });
|
||||
fireEvent.click(sw);
|
||||
await waitFor(() => expect(mockCronUpdate).toHaveBeenCalledWith('tp-1', { enabled: true }));
|
||||
});
|
||||
|
||||
it('reverts the autopilot toggle when the cron update fails', async () => {
|
||||
mockCronUpdate.mockRejectedValueOnce(new Error('boom'));
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
const sw = await screen.findByRole('switch', { name: /run automatically/i });
|
||||
fireEvent.click(sw);
|
||||
// The update RPC must actually be attempted (and fail)…
|
||||
await waitFor(() => expect(mockCronUpdate).toHaveBeenCalledWith('tp-1', { enabled: true }));
|
||||
// …then the optimistic flip reverts to off after the failure settles.
|
||||
await waitFor(() => expect(sw).toHaveAttribute('aria-checked', 'false'));
|
||||
});
|
||||
|
||||
it('hides the autopilot toggle when no seeded job exists', async () => {
|
||||
mockCronList.mockResolvedValue({ result: [], logs: [] });
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await screen.findByText('Confine to workspace');
|
||||
expect(screen.queryByRole('switch', { name: /run automatically/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adding then removing a granted folder persists the updated list', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await screen.findByText('Granted folders');
|
||||
|
||||
@@ -4343,6 +4343,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'الموافقة على خطة العمل المطلوبة',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'وقف أمام عميل معين يقوم بتنفيذ موجز عمل مشرف على عميل',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'وكيل tiny.place المستقل',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'دع OpenHuman يتصرف على tiny.place بمفرده: وفق جدول زمني يبحث عن عمل مجدٍ — المكافآت المفتوحة أولًا —، وينجز ما يناسب مهاراته ويتصرف من هويتك. يعمل دون إشراف ويمكنه الإنفاق، لذا أبقِه على devnet أثناء الاختبار. معطّل افتراضيًا.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'التشغيل تلقائيًا',
|
||||
'settings.agentAccess.timeout.label': 'مهلة الإجراء',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'المدة التي يُسمح خلالها بتشغيل أداة أو إجراء واحد قبل إلغائه. زِد هذه القيمة إذا كان نموذج محلي كبير يُقاطَع قبل أن ينهي رده.',
|
||||
|
||||
@@ -4432,6 +4432,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'কাজের পরিকল্পনা অনুমোদন প্রয়োজন',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'নির্ধারিত কর্মের পূর্বে একটি author-ed কর্মের সঞ্চালনার পূর্বে কর্ম স্থগিত করা হবে।',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'স্বয়ংক্রিয় tiny.place এজেন্ট',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'OpenHuman-কে tiny.place-এ নিজে কাজ করতে দিন: এটি নির্ধারিত সময় অনুযায়ী মূল্যবান কাজ খোঁজে — প্রথমে খোলা বাউন্টি —, নিজের দক্ষতার সাথে মানানসই কাজ করে এবং আপনার পরিচয় থেকে কাজ করে। এটি তত্ত্বাবধান ছাড়াই চলে এবং অর্থ ব্যয় করতে পারে, তাই পরীক্ষার সময় এটি devnet-এ রাখুন। ডিফল্টভাবে বন্ধ।',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'স্বয়ংক্রিয়ভাবে চালান',
|
||||
'settings.agentAccess.timeout.label': 'অ্যাকশন টাইমআউট',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'একটি একক টুল বা অ্যাকশন বাতিল হওয়ার আগে কতক্ষণ চলতে পারে। বড় লোকাল মডেল উত্তর শেষ করার আগেই থেমে গেলে এটি বাড়ান।',
|
||||
|
||||
@@ -4547,6 +4547,10 @@ const messages: TranslationMap = {
|
||||
'Erfordern Sie die Genehmigung des Aufgabenplans',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Pausieren Sie, bevor ein zugewiesener Agent ein vom Agenten verfasstes Aufgaben-Briefing ausführt.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Autonomer tiny.place-Agent',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Lass OpenHuman eigenständig auf tiny.place handeln: zeitgesteuert sucht es lohnende Arbeit – zuerst offene Bounties –, erledigt Passendes und handelt über deine Identität. Es läuft unbeaufsichtigt und kann Geld ausgeben; nutze beim Testen devnet. Standardmäßig aus.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Automatisch ausführen',
|
||||
'settings.agentAccess.timeout.label': 'Aktions-Timeout',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Wie lange ein einzelnes Werkzeug oder eine Aktion laufen darf, bevor sie abgebrochen wird. Erhöhen Sie diesen Wert, wenn ein großes lokales Modell unterbrochen wird, bevor es seine Antwort beendet.',
|
||||
|
||||
@@ -5032,6 +5032,10 @@ const en: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Pause before an assigned agent executes an agent-authored task brief.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Autonomous tiny.place agent',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Let OpenHuman act on tiny.place on its own: on a schedule it finds worthwhile work — open bounties first — does what fits its skills, and acts from your identity. It runs unattended and can spend, so keep it on devnet while testing. Off by default.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Run automatically',
|
||||
'settings.agentAccess.timeout.label': 'Action timeout',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'How long a single tool or action may run before it is cancelled. Increase this if a large local model is interrupted before it finishes responding.',
|
||||
|
||||
@@ -4510,6 +4510,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'Requerir la aprobación del plan de tareas',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Pausa antes de que un agente asignado ejecute un breve tarea elaborada por el agente.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Agente autónomo de tiny.place',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Deja que OpenHuman actúe en tiny.place por su cuenta: de forma programada busca trabajo que valga la pena —recompensas abiertas primero—, hace lo que encaja con sus habilidades y actúa desde tu identidad. Funciona sin supervisión y puede gastar, así que mantenlo en devnet mientras pruebas. Desactivado por defecto.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Ejecutar automáticamente',
|
||||
'settings.agentAccess.timeout.label': 'Tiempo de espera de la acción',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Cuánto tiempo puede ejecutarse una sola herramienta o acción antes de cancelarse. Aumenta este valor si un modelo local grande se interrumpe antes de terminar su respuesta.',
|
||||
|
||||
@@ -4528,6 +4528,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': "Exiger l'approbation du plan de tâche",
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
"Pause avant qu'un agent assigné n'exécute un briefing de tâche rédigé par un agent.",
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Agent tiny.place autonome',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Laissez OpenHuman agir sur tiny.place tout seul : de façon planifiée, il cherche du travail intéressant — les primes ouvertes d’abord —, fait ce qui correspond à ses compétences et agit depuis votre identité. Il fonctionne sans surveillance et peut dépenser ; gardez-le sur devnet pendant vos tests. Désactivé par défaut.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Exécuter automatiquement',
|
||||
'settings.agentAccess.timeout.label': "Délai d'expiration de l'action",
|
||||
'settings.agentAccess.timeout.desc':
|
||||
"Durée pendant laquelle un seul outil ou une seule action peut s'exécuter avant d'être annulé. Augmentez cette valeur si un grand modèle local est interrompu avant d'avoir terminé sa réponse.",
|
||||
|
||||
@@ -4438,6 +4438,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'कार्य योजना अनुमोदन की आवश्यकता',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'एक निर्धारित एजेंट से पहले रोकें एक एजेंट-लेखित कार्य संक्षिप्त निष्पादित करता है।',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'स्वायत्त tiny.place एजेंट',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'OpenHuman को tiny.place पर स्वयं कार्य करने दें: यह निर्धारित समय पर सार्थक काम ढूँढता है — पहले खुली बाउंटी —, अपनी क्षमताओं के अनुरूप काम करता है और आपकी पहचान से कार्य करता है। यह बिना निगरानी के चलता है और खर्च कर सकता है, इसलिए परीक्षण के दौरान इसे devnet पर रखें। डिफ़ॉल्ट रूप से बंद।',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'स्वतः चलाएँ',
|
||||
'settings.agentAccess.timeout.label': 'क्रिया टाइमआउट',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'किसी एकल टूल या क्रिया को रद्द होने से पहले कितनी देर चलने दिया जाए। यदि कोई बड़ा लोकल मॉडल अपना उत्तर पूरा करने से पहले रुक जाता है तो इसे बढ़ाएँ।',
|
||||
|
||||
@@ -4445,6 +4445,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'Perlu persetujuan rencana tugas',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Jeda sebelum agen yang ditugaskan mengeksekusi suatu tugas singkat.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Agen tiny.place otonom',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Biarkan OpenHuman bertindak di tiny.place sendiri: secara terjadwal ia mencari pekerjaan yang berharga — bounty terbuka lebih dulu —, mengerjakan yang sesuai keahliannya, dan bertindak dari identitas Anda. Ia berjalan tanpa pengawasan dan dapat membelanjakan dana, jadi gunakan devnet saat menguji. Nonaktif secara bawaan.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Jalankan otomatis',
|
||||
'settings.agentAccess.timeout.label': 'Batas waktu tindakan',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Berapa lama satu alat atau tindakan boleh berjalan sebelum dibatalkan. Tingkatkan nilai ini jika model lokal besar terhenti sebelum selesai merespons.',
|
||||
|
||||
@@ -4503,6 +4503,10 @@ const messages: TranslationMap = {
|
||||
"Richiedere l'approvazione del piano di lavoro",
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
"Pausa prima che un agente assegnato esegua un brief del compito scritto dall'agente.",
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Agente tiny.place autonomo',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Lascia che OpenHuman agisca su tiny.place da solo: in modo pianificato cerca lavoro utile — prima le taglie aperte —, fa ciò che si adatta alle sue competenze e agisce dalla tua identità. Funziona senza supervisione e può spendere, quindi tienilo su devnet durante i test. Disattivato per impostazione predefinita.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Esegui automaticamente',
|
||||
'settings.agentAccess.timeout.label': "Timeout dell'azione",
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Per quanto tempo un singolo strumento o azione può essere eseguito prima di essere annullato. Aumenta questo valore se un modello locale di grandi dimensioni viene interrotto prima di completare la risposta.',
|
||||
|
||||
@@ -4392,6 +4392,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': '작업 계획 승인 필요',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'할당된 에이전트가 에이전트가 작성한 작업 브리프를 실행하기 전에 일시 중지합니다.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': '자율 tiny.place 에이전트',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'OpenHuman이 tiny.place에서 스스로 행동하게 하세요: 일정에 따라 가치 있는 일을 찾고(열린 현상금 우선) 자신의 능력에 맞는 작업을 수행하며 당신의 신원으로 행동합니다. 감독 없이 작동하며 비용을 지출할 수 있으니 테스트 중에는 devnet을 사용하세요. 기본값은 꺼짐입니다.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': '자동 실행',
|
||||
'settings.agentAccess.timeout.label': '작업 제한 시간',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'단일 도구나 작업이 취소되기 전까지 실행될 수 있는 시간입니다. 대형 로컬 모델이 응답을 끝내기 전에 중단되는 경우 이 값을 늘리세요.',
|
||||
|
||||
@@ -4499,6 +4499,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'Wymagaj zatwierdzenia planu zadania',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Wstrzymaj, zanim przypisany agent wykona opis zadania utworzony przez agenta.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Autonomiczny agent tiny.place',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Pozwól OpenHuman działać na tiny.place samodzielnie: zgodnie z harmonogramem szuka wartościowej pracy — najpierw otwartych nagród —, wykonuje to, co pasuje do jego umiejętności, i działa z Twojej tożsamości. Działa bez nadzoru i może wydawać środki, więc podczas testów używaj devnet. Domyślnie wyłączone.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Uruchamiaj automatycznie',
|
||||
'settings.agentAccess.timeout.label': 'Limit czasu akcji',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Jak długo pojedyncze narzędzie lub akcja może działać przed anulowaniem. Zwiększ tę wartość, jeśli duży lokalny model jest przerywany, zanim zakończy odpowiedź.',
|
||||
|
||||
@@ -4504,6 +4504,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'Exigir aprovação do plano de tarefas',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Pausa antes que um agente designado execute um briefing de tarefa elaborado pelo agente.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Agente autônomo do tiny.place',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Deixe o OpenHuman agir no tiny.place sozinho: de forma agendada ele busca trabalho que valha a pena — recompensas abertas primeiro —, faz o que combina com suas habilidades e age a partir da sua identidade. Funciona sem supervisão e pode gastar, então mantenha-o na devnet durante os testes. Desativado por padrão.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Executar automaticamente',
|
||||
'settings.agentAccess.timeout.label': 'Tempo limite da ação',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Por quanto tempo uma única ferramenta ou ação pode ser executada antes de ser cancelada. Aumente este valor se um modelo local grande for interrompido antes de terminar a resposta.',
|
||||
|
||||
@@ -4473,6 +4473,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': 'Требовать утверждения плана задач',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'Сделайте паузу перед тем, как назначенный агент выполнит задание, созданное агентом.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': 'Автономный агент tiny.place',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'Позвольте OpenHuman действовать в tiny.place самостоятельно: по расписанию он ищет стоящую работу — сначала открытые награды —, выполняет подходящее его навыкам и действует от вашего имени. Работает без присмотра и может тратить средства, поэтому при тестировании используйте devnet. По умолчанию выключено.',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': 'Запускать автоматически',
|
||||
'settings.agentAccess.timeout.label': 'Тайм-аут действия',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'Сколько времени может выполняться отдельный инструмент или действие до отмены. Увеличьте это значение, если крупная локальная модель прерывается до завершения ответа.',
|
||||
|
||||
@@ -4217,6 +4217,10 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.requireTaskPlanApproval.label': '要求批准任务计划',
|
||||
'settings.agentAccess.requireTaskPlanApproval.desc':
|
||||
'在指定智能体执行由智能体编写的任务简报前暂停。',
|
||||
'settings.agentAccess.tinyplaceAutopilot.title': '自主 tiny.place 代理',
|
||||
'settings.agentAccess.tinyplaceAutopilot.desc':
|
||||
'让 OpenHuman 自行在 tiny.place 上行动:按计划寻找有价值的工作(优先开放的悬赏),完成符合其技能的任务,并以你的身份行动。它在无人监督下运行且可以花费资金,测试时请使用 devnet。默认关闭。',
|
||||
'settings.agentAccess.tinyplaceAutopilot.label': '自动运行',
|
||||
'settings.agentAccess.timeout.label': '操作超时',
|
||||
'settings.agentAccess.timeout.desc':
|
||||
'单个工具或操作在被取消前可运行的时长。如果大型本地模型在完成响应前被中断,请增大此值。',
|
||||
|
||||
@@ -2062,6 +2062,14 @@ async fn run_server_inner(
|
||||
return;
|
||||
}
|
||||
log::info!("[cron] spawning scheduler polling loop");
|
||||
// Ensure proactive agent jobs (e.g. the autonomous bounty job)
|
||||
// exist for already-onboarded users upgrading from a build that
|
||||
// predates them — otherwise their Settings toggle stays hidden.
|
||||
// Idempotent; no-op until onboarding is complete.
|
||||
if let Err(e) = crate::openhuman::cron::seed::seed_proactive_agents_on_boot(&config)
|
||||
{
|
||||
log::warn!("[cron] boot seed of proactive agent jobs failed: {e}");
|
||||
}
|
||||
if let Err(e) = crate::openhuman::cron::scheduler::run(config).await {
|
||||
log::error!("[cron] scheduler loop ended with error: {e}");
|
||||
}
|
||||
|
||||
@@ -356,6 +356,8 @@ fn handle_add(params: Map<String, Value>) -> ControllerFuture {
|
||||
delivery,
|
||||
delete_after_run,
|
||||
agent_id,
|
||||
// RPC-created jobs default to enabled (current behaviour).
|
||||
true,
|
||||
)
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ use anyhow::Result;
|
||||
/// Well-known job names used to detect whether seeding has already run.
|
||||
const MORNING_BRIEFING_JOB_NAME: &str = "morning_briefing";
|
||||
|
||||
/// Well-known name of the opt-in autonomous tiny.place agent job ("autopilot").
|
||||
/// Generic on purpose: it runs `tinyplace_agent`, which can do anything on
|
||||
/// tiny.place — bounties are just its default activity, not its limit.
|
||||
const TINYPLACE_AUTOPILOT_JOB_NAME: &str = "tinyplace_autopilot";
|
||||
|
||||
/// Legacy name of the one-shot welcome cron job created by earlier
|
||||
/// builds of `seed_proactive_agents`. Kept as a constant (rather than
|
||||
/// a string literal inline) so a grep for `WELCOME_JOB_NAME` still
|
||||
@@ -74,9 +79,49 @@ pub fn seed_proactive_agents(config: &Config) -> Result<()> {
|
||||
tracing::debug!("[cron::seed] morning_briefing job already exists — skipping");
|
||||
}
|
||||
|
||||
if !has(TINYPLACE_AUTOPILOT_JOB_NAME) {
|
||||
tracing::info!(
|
||||
"[cron::seed] creating autonomous tiny.place autopilot job (tinyplace_agent, disabled — opt-in)"
|
||||
);
|
||||
seed_tinyplace_autopilot(config)?;
|
||||
} else {
|
||||
tracing::debug!("[cron::seed] tinyplace_autopilot job already exists — skipping");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Boot-time entry point: backfill the autopilot job for an already-onboarded
|
||||
/// user who upgraded from a build that predates it.
|
||||
///
|
||||
/// Crucially this does **not** replay the full onboarding seed set
|
||||
/// ([`seed_proactive_agents`]) — a user may have *deliberately removed* a
|
||||
/// default job (e.g. `morning_briefing` via Settings → Cron Jobs), and
|
||||
/// re-creating it on every boot would silently override that opt-out. So we only
|
||||
/// ensure the one job this build introduces (`tinyplace_autopilot`) exists.
|
||||
/// Future default jobs should get their own narrow boot-backfill like this one.
|
||||
///
|
||||
/// No-op until onboarding is complete (a fresh user is seeded by the
|
||||
/// `false→true` transition, [`set_onboarding_completed`]) and idempotent (skips
|
||||
/// if the autopilot job already exists).
|
||||
///
|
||||
/// [`set_onboarding_completed`]: crate::openhuman::config::ops::ui::set_onboarding_completed
|
||||
pub fn seed_proactive_agents_on_boot(config: &Config) -> Result<()> {
|
||||
if !config.onboarding_completed {
|
||||
tracing::debug!("[cron::seed] boot seed skipped — onboarding not complete");
|
||||
return Ok(());
|
||||
}
|
||||
let exists = list_jobs(config)?
|
||||
.iter()
|
||||
.any(|j| j.name.as_deref() == Some(TINYPLACE_AUTOPILOT_JOB_NAME));
|
||||
if exists {
|
||||
tracing::debug!("[cron::seed] boot seed — tinyplace_autopilot already present, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
tracing::info!("[cron::seed] boot seed — backfilling tinyplace_autopilot (disabled, opt-in)");
|
||||
seed_tinyplace_autopilot(config)
|
||||
}
|
||||
|
||||
/// Remove any persisted cron job named `"welcome"` from a prior build.
|
||||
///
|
||||
/// The one-shot welcome job `delete_after_run = true + Schedule::At`
|
||||
@@ -143,11 +188,72 @@ fn seed_morning_briefing(config: &Config) -> Result<()> {
|
||||
Some(proactive_delivery()),
|
||||
false, // recurring — do not delete after run
|
||||
Some(MORNING_BRIEFING_JOB_NAME.to_string()),
|
||||
true, // enabled
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Seed the autonomous tiny.place "autopilot" as a recurring (hourly) agent job
|
||||
/// — created **disabled**.
|
||||
///
|
||||
/// The job runs `tinyplace_agent` (the single tiny.place agent) autonomously.
|
||||
/// It's named generically (`tinyplace_autopilot`) because the agent can do
|
||||
/// anything on tiny.place — bounties are its default activity, not its limit.
|
||||
///
|
||||
/// This is opt-in for a reason: a cron run bypasses the approval gate, and
|
||||
/// `tinyplace_agent`'s prompt authorizes it to take paid/irreversible actions
|
||||
/// when running autonomously — and money on tiny.place is real x402/SPL spend.
|
||||
/// The safety rails are therefore (1) this opt-in toggle, off by default until
|
||||
/// the user enables it via the Settings switch (cron.update_job → enabled=true),
|
||||
/// and (2) the devnet-first, be-prudent guidance in the agent's prompt.
|
||||
///
|
||||
/// Runs in an isolated session with `proactive` delivery so each cycle's report
|
||||
/// (what it worked on, submission URLs/IDs, anything it funded) reaches the
|
||||
/// user's active channel via the channels module's `ProactiveMessageSubscriber`.
|
||||
fn seed_tinyplace_autopilot(config: &Config) -> Result<()> {
|
||||
tracing::debug!("[cron::seed] seed_tinyplace_autopilot start");
|
||||
let schedule = Schedule::Every {
|
||||
every_ms: 60 * 60 * 1000, // hourly
|
||||
};
|
||||
|
||||
let prompt = concat!(
|
||||
"Run an autonomous tiny.place session. Confirm your identity and check ",
|
||||
"your status/inbox, then look for worthwhile work — open bounties are ",
|
||||
"the main opportunity, so recall which you've already attempted, pick the ",
|
||||
"top 1-2 open ones that fit your skills, do the work, publish each ",
|
||||
"deliverable to your feed (tinyplace_post), and submit it. You are running ",
|
||||
"autonomously, so you may take paid actions when worthwhile — be prudent ",
|
||||
"with funds and prefer devnet. Record what you do in memory and report ",
|
||||
"concrete results (submission URLs/IDs, anything funded)."
|
||||
);
|
||||
|
||||
// Insert already-disabled (enabled=false) in a single statement. Opt-in is
|
||||
// load-bearing for an autonomous spender, so we never create it enabled and
|
||||
// then disable it in a second write — a crash between the two could leave it
|
||||
// running without the user opting in.
|
||||
let job = add_agent_job_with_definition(
|
||||
config,
|
||||
Some(TINYPLACE_AUTOPILOT_JOB_NAME.to_string()),
|
||||
schedule,
|
||||
prompt,
|
||||
SessionTarget::Isolated,
|
||||
None,
|
||||
Some(proactive_delivery()),
|
||||
false, // recurring — do not delete after run
|
||||
// Runs the single tiny.place agent autonomously (no dedicated agent def).
|
||||
Some("tinyplace_agent".to_string()),
|
||||
false, // enabled=false — opt-in, created disabled atomically
|
||||
)?;
|
||||
|
||||
tracing::debug!(
|
||||
job_id = %job.id,
|
||||
enabled = job.enabled,
|
||||
"[cron::seed] seed_tinyplace_autopilot done — created disabled (opt-in)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -184,6 +290,123 @@ mod tests {
|
||||
assert!(d.best_effort);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeds_tinyplace_autopilot_disabled_and_idempotent() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = test_config(&tmp);
|
||||
|
||||
seed_proactive_agents(&config).expect("first seed");
|
||||
let jobs = list_jobs(&config).unwrap();
|
||||
let worker: Vec<_> = jobs
|
||||
.iter()
|
||||
.filter(|j| j.name.as_deref() == Some(TINYPLACE_AUTOPILOT_JOB_NAME))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
worker.len(),
|
||||
1,
|
||||
"exactly one tinyplace_autopilot job, got {worker:?}"
|
||||
);
|
||||
let worker = worker[0];
|
||||
// Opt-in: must be created disabled.
|
||||
assert!(
|
||||
!worker.enabled,
|
||||
"tinyplace_autopilot must be seeded disabled (opt-in)"
|
||||
);
|
||||
// Runs the single tiny.place agent autonomously (no dedicated agent def).
|
||||
assert_eq!(worker.agent_id.as_deref(), Some("tinyplace_agent"));
|
||||
|
||||
// Idempotent: a second seed must not create a duplicate.
|
||||
seed_proactive_agents(&config).expect("second seed");
|
||||
let after = list_jobs(&config).unwrap();
|
||||
assert_eq!(
|
||||
after
|
||||
.iter()
|
||||
.filter(|j| j.name.as_deref() == Some(TINYPLACE_AUTOPILOT_JOB_NAME))
|
||||
.count(),
|
||||
1,
|
||||
"second seed must not duplicate the tinyplace_autopilot job"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boot_seed_is_noop_until_onboarded() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut config = test_config(&tmp);
|
||||
config.onboarding_completed = false;
|
||||
|
||||
seed_proactive_agents_on_boot(&config).expect("boot seed");
|
||||
assert!(
|
||||
list_jobs(&config).unwrap().is_empty(),
|
||||
"boot seed must not create jobs before onboarding completes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boot_seed_creates_missing_jobs_when_onboarded() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut config = test_config(&tmp);
|
||||
config.onboarding_completed = true;
|
||||
|
||||
seed_proactive_agents_on_boot(&config).expect("boot seed");
|
||||
let jobs = list_jobs(&config).unwrap();
|
||||
// The autonomous tiny.place job exists, disabled (opt-in), on tinyplace_agent.
|
||||
let worker = jobs
|
||||
.iter()
|
||||
.find(|j| j.name.as_deref() == Some(TINYPLACE_AUTOPILOT_JOB_NAME))
|
||||
.expect("tinyplace_autopilot job should be seeded on boot when onboarded");
|
||||
assert!(!worker.enabled);
|
||||
assert_eq!(worker.agent_id.as_deref(), Some("tinyplace_agent"));
|
||||
|
||||
// Idempotent across a second boot.
|
||||
seed_proactive_agents_on_boot(&config).expect("second boot seed");
|
||||
assert_eq!(
|
||||
list_jobs(&config)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|j| j.name.as_deref() == Some(TINYPLACE_AUTOPILOT_JOB_NAME))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
// Boot-backfill is scoped to the autopilot — it must NOT replay the full
|
||||
// onboarding seed set, so it never created morning_briefing here.
|
||||
assert!(
|
||||
!list_jobs(&config)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|j| j.name.as_deref() == Some(MORNING_BRIEFING_JOB_NAME)),
|
||||
"boot backfill must not seed morning_briefing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boot_seed_does_not_recreate_a_removed_default_job() {
|
||||
// Regression: a user who deliberately removed morning_briefing must not
|
||||
// have it silently recreated on the next core start by the boot backfill.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut config = test_config(&tmp);
|
||||
config.onboarding_completed = true;
|
||||
|
||||
// Full onboarding seed, then the user removes morning_briefing.
|
||||
seed_proactive_agents(&config).expect("onboarding seed");
|
||||
let mb_id = list_jobs(&config)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|j| j.name.as_deref() == Some(MORNING_BRIEFING_JOB_NAME))
|
||||
.expect("morning_briefing seeded")
|
||||
.id;
|
||||
remove_job(&config, &mb_id).expect("remove morning_briefing");
|
||||
|
||||
// Boot backfill must leave the opt-out intact.
|
||||
seed_proactive_agents_on_boot(&config).expect("boot seed");
|
||||
assert!(
|
||||
!list_jobs(&config)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|j| j.name.as_deref() == Some(MORNING_BRIEFING_JOB_NAME)),
|
||||
"boot backfill must not resurrect a user-removed morning_briefing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_prunes_legacy_welcome_job() {
|
||||
// Simulate the state an earlier build would have left behind:
|
||||
@@ -205,6 +428,7 @@ mod tests {
|
||||
Some(proactive_delivery()),
|
||||
true,
|
||||
Some(LEGACY_WELCOME_JOB_NAME.to_string()),
|
||||
true, // enabled
|
||||
)
|
||||
.expect("seed legacy welcome");
|
||||
assert_eq!(list_jobs(&config).unwrap().len(), 1);
|
||||
|
||||
@@ -78,6 +78,7 @@ pub fn add_agent_job(
|
||||
delivery,
|
||||
delete_after_run,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,6 +96,7 @@ pub fn add_agent_job_with_definition(
|
||||
delivery: Option<DeliveryConfig>,
|
||||
delete_after_run: bool,
|
||||
agent_id: Option<String>,
|
||||
enabled: bool,
|
||||
) -> Result<CronJob> {
|
||||
let now = Utc::now();
|
||||
validate_schedule(&schedule, now)?;
|
||||
@@ -105,11 +107,15 @@ pub fn add_agent_job_with_definition(
|
||||
let delivery = delivery.unwrap_or_default();
|
||||
|
||||
with_connection(config, |conn| {
|
||||
// `enabled` is bound (?13) rather than hard-coded so callers can insert a
|
||||
// job in its final disabled state in one statement — important for opt-in
|
||||
// jobs (e.g. the autopilot) where a create-then-disable sequence could
|
||||
// leave the row enabled if the process died between the two writes.
|
||||
conn.execute(
|
||||
"INSERT INTO cron_jobs (
|
||||
id, expression, command, schedule, job_type, prompt, name, session_target, model,
|
||||
enabled, delivery, delete_after_run, created_at, next_run, agent_id
|
||||
) VALUES (?1, ?2, '', ?3, 'agent', ?4, ?5, ?6, ?7, 1, ?8, ?9, ?10, ?11, ?12)",
|
||||
) VALUES (?1, ?2, '', ?3, 'agent', ?4, ?5, ?6, ?7, ?13, ?8, ?9, ?10, ?11, ?12)",
|
||||
params![
|
||||
id,
|
||||
expression,
|
||||
@@ -123,6 +129,7 @@ pub fn add_agent_job_with_definition(
|
||||
now.to_rfc3339(),
|
||||
next_run.to_rfc3339(),
|
||||
agent_id,
|
||||
if enabled { 1 } else { 0 },
|
||||
],
|
||||
)
|
||||
.context("Failed to insert cron agent job")?;
|
||||
|
||||
@@ -37,6 +37,8 @@ named = [
|
||||
"tinyplace_create_group",
|
||||
"tinyplace_post_bounty",
|
||||
"tinyplace_submit_work",
|
||||
# Publish a deliverable to your own feed → returns a shareable URL to submit.
|
||||
"tinyplace_post",
|
||||
"tinyplace_submissions",
|
||||
"tinyplace_job_apply",
|
||||
# ── Batched read gateway + escape hatch + manual ──
|
||||
@@ -45,6 +47,11 @@ named = [
|
||||
"tinyplace_help",
|
||||
# Confirmation gate for paid/irreversible tiny.place actions.
|
||||
"ask_user_clarification",
|
||||
# Durable state across runs — lets a scheduled/autonomous run remember which
|
||||
# bounties it already attempted (dedupe) and what worked.
|
||||
"memory_store",
|
||||
"memory_recall",
|
||||
"memory_search",
|
||||
# Deterministic time grounding for job deadlines, status windows, and
|
||||
# recency phrasing in tiny.place messages or work listings.
|
||||
"resolve_time",
|
||||
|
||||
@@ -10,10 +10,11 @@ Own tiny.place identity registration, directory/profile lookup, Agent Cards, mar
|
||||
|
||||
Your tools return **markdown**, not JSON, and each ends with a `## Next steps` block of ready-to-run follow-up calls (tool name + exact args) — read it to decide what to do next.
|
||||
|
||||
- **Flows** (one call = one task): `tinyplace_whoami`, `tinyplace_status` (your recurring check-in), `tinyplace_discover`, `tinyplace_search`, `tinyplace_feed`, `tinyplace_find_work`, `tinyplace_messages`, `tinyplace_register`, `tinyplace_follow` / `tinyplace_unfollow`, `tinyplace_join_group` / `tinyplace_create_group`, `tinyplace_post_bounty`, `tinyplace_submit_work`, `tinyplace_submissions`, `tinyplace_job_apply`.
|
||||
- **Flows** (one call = one task): `tinyplace_whoami`, `tinyplace_status` (your recurring check-in), `tinyplace_discover`, `tinyplace_search`, `tinyplace_feed`, `tinyplace_find_work`, `tinyplace_messages`, `tinyplace_register`, `tinyplace_follow` / `tinyplace_unfollow`, `tinyplace_join_group` / `tinyplace_create_group`, `tinyplace_post_bounty`, `tinyplace_submit_work`, `tinyplace_post` (publish a deliverable to your own feed → returns a shareable URL to submit), `tinyplace_submissions`, `tinyplace_job_apply`.
|
||||
- **`tinyplace_graphql`** — the batched read gateway (home feed, posts, agents, identities, jobs, bounties, products, ledger). Use it for any read a flow doesn't already cover.
|
||||
- **`tinyplace_call`** — the escape hatch: invoke any underlying controller by name (`command` + `params`) for the long tail. Run `tinyplace_help` with `topic='commands'` for the catalog.
|
||||
- **`tinyplace_call`** — the escape hatch: invoke any underlying controller by name (`command` + `params`) for the long tail (marketplace buy/bid/offer, escrow, etc.). Run `tinyplace_help` with `topic='commands'` for the catalog.
|
||||
- **`tinyplace_help`** — the operating manual. When unsure how something works (onboarding, run-loop, bounties, payments, messaging), read the relevant topic first.
|
||||
- **`memory_store` / `memory_recall` / `memory_search`** — durable state across runs. On a scheduled/autonomous run, recall what you already attempted before acting (so you don't repeat work or double-submit) and store what you did afterwards.
|
||||
|
||||
## Typical Flow
|
||||
|
||||
@@ -21,7 +22,9 @@ Your tools return **markdown**, not JSON, and each ends with a `## Next steps` b
|
||||
2. Prefer a curated flow; fall back to `tinyplace_graphql` for reads and `tinyplace_call` for the long tail. Use tiny.place tools only — never route tiny.place actions through generic shell, HTTP, Composio, MCP, crypto, or market agents.
|
||||
3. Follow the `Next steps` suggestions returned by each tool to chain multi-step tasks (e.g. find_work → submit_work → watch the bounty).
|
||||
4. For writes, explain the exact action before calling when intent is ambiguous.
|
||||
5. For paid, irreversible, or human-only accept/approve/select actions, stop for explicit user confirmation before execution. A **Payment required** block is incomplete work, not success — surface the asset, amount, network, and retry action instead of claiming completion.
|
||||
5. **Paid / irreversible actions depend on how you're running:**
|
||||
- **Interactive (a person is in the loop):** don't invent your own confirmation step — the platform's approval gate automatically surfaces every paid/irreversible action for the user to Approve/Deny before anything settles. Explain the action, then call it and let the gate handle confirmation. A **Payment required** block is a fund-and-retry step — surface the asset, amount, network, and retry action.
|
||||
- **Autonomous (a scheduled/background run instructs you to act on your own):** there is no human to confirm, and you are **authorized to take paid/irreversible actions yourself**. Be prudent: prefer free paths, spend only what a step genuinely needs, default to devnet unless clearly configured otherwise, never expose wallet secrets, and never fabricate a result.
|
||||
6. Report concrete IDs returned by tools: job IDs, proposal IDs, escrow IDs, message IDs, bounty IDs, handles, and transaction/payment references.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Write tiny.place flows: register, follow/unfollow, join/create group,
|
||||
//! post_bounty, submit_work, submissions, job_apply.
|
||||
//! post_bounty, submit_work, post, submissions, job_apply.
|
||||
//!
|
||||
//! All declare `Write` + external effect, so the agent harness routes them
|
||||
//! through the `ApprovalGate`. Identity is always taken from the wallet signer
|
||||
@@ -128,6 +128,25 @@ pub fn write_tools() -> Vec<Box<dyn Tool>> {
|
||||
submit_work_flow,
|
||||
)
|
||||
.boxed(),
|
||||
FlowTool::write(
|
||||
"tinyplace_post",
|
||||
"Publish a post to your own tiny.place feed and get back a shareable \
|
||||
URL. Use this to host a bounty deliverable: post your finished work, \
|
||||
then submit the returned URL with tinyplace_submit_work. Posting is \
|
||||
free and goes to your own feed (no @handle required).",
|
||||
json!({
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"body": { "type": "string", "description": "The post content — your work / deliverable. Markdown is fine." },
|
||||
"content_type": { "type": "string", "description": "Optional content-type hint, e.g. 'text/markdown'." },
|
||||
"bounty_id": { "type": "string", "description": "Optional: the bounty this deliverable is for — pre-fills the submit suggestion with this URL." }
|
||||
},
|
||||
"required": ["body"]
|
||||
}),
|
||||
post_flow,
|
||||
)
|
||||
.boxed(),
|
||||
// A read (list_submissions) — registered as a read flow so it isn't
|
||||
// approval-gated; the suggested `bounties_run_council` action is gated
|
||||
// on its own through `tinyplace_call`.
|
||||
@@ -510,6 +529,74 @@ fn submit_work_flow(args: Value) -> FlowFuture {
|
||||
})
|
||||
}
|
||||
|
||||
/// Public, judge-facing web origin for tiny.place post permalinks. The SDK base
|
||||
/// URL points at the backend API (e.g. `api.tiny.place`); the human/council-
|
||||
/// facing site is `tiny.place` (same origin the wallet fund page uses). We build
|
||||
/// the deliverable URL from the returned post id so a bounty submission resolves
|
||||
/// to viewable work.
|
||||
const TINYPLACE_WEB_ORIGIN: &str = "https://tiny.place";
|
||||
|
||||
fn post_flow(args: Value) -> FlowFuture {
|
||||
Box::pin(async move {
|
||||
let body = req_str(&args, "body")?;
|
||||
if body.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!("missing required param 'body'"));
|
||||
}
|
||||
let content_type = opt_str(&args, "content_type");
|
||||
let bounty_id = opt_str(&args, "bounty_id");
|
||||
log::debug!(
|
||||
"[tinyplace][flow] post start body_len={} bounty={:?}",
|
||||
body.len(),
|
||||
bounty_id
|
||||
);
|
||||
let client = client().await?;
|
||||
// Post to the SIGNER's OWN feed: the handle is the wallet agent id, which
|
||||
// is a valid feed handle for every wallet — registered @handle or not —
|
||||
// and a caller can only ever post to a feed it owns. Mirrors the
|
||||
// `feeds_create_post` controller.
|
||||
let handle = agent_id(client)?;
|
||||
let post_create = tinyplace::types::PostCreate {
|
||||
body,
|
||||
content_type,
|
||||
post_id: None,
|
||||
};
|
||||
log::debug!("[tinyplace][flow] post create_post_call handle={handle}");
|
||||
match client.feeds.create_post(&handle, &post_create).await {
|
||||
Ok(post) => {
|
||||
let post_id = post.post_id.clone();
|
||||
let url = format!("{TINYPLACE_WEB_ORIGIN}/posts/{post_id}");
|
||||
log::debug!(
|
||||
"[tinyplace][flow] post create_post_ok post_id={post_id} bounty={bounty_id:?}"
|
||||
);
|
||||
let v = serde_json::to_value(&post).unwrap_or(Value::Null);
|
||||
let mut md = Markdown::new();
|
||||
md.heading("Published to your feed");
|
||||
md.kv([("Post URL", url.clone()), ("Post id", post_id)]);
|
||||
md.raw_section(render_json(&v));
|
||||
// If this post is a bounty deliverable, pre-fill the submit call
|
||||
// with the fresh URL so there are no placeholders to fill in.
|
||||
let suggestion = match bounty_id {
|
||||
Some(id) => Suggestion::new(
|
||||
format!("Submit this as your work for {id}"),
|
||||
"tinyplace_submit_work",
|
||||
json!({ "bounty_id": id, "url": url }),
|
||||
),
|
||||
None => Suggestion::new(
|
||||
"Read your feed",
|
||||
"tinyplace_feed",
|
||||
json!({ "include_self": true }),
|
||||
),
|
||||
};
|
||||
finish(md, &[suggestion])
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("[tinyplace][flow] post create_post_err err={e}");
|
||||
Ok(sdk_error("Publishing your post", e))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn submissions_flow(args: Value) -> FlowFuture {
|
||||
Box::pin(async move {
|
||||
let bounty_id = req_str(&args, "bounty_id")?;
|
||||
|
||||
@@ -67,6 +67,7 @@ mod tests {
|
||||
"tinyplace_follow",
|
||||
"tinyplace_post_bounty",
|
||||
"tinyplace_submit_work",
|
||||
"tinyplace_post",
|
||||
"tinyplace_job_apply",
|
||||
"tinyplace_graphql",
|
||||
"tinyplace_call",
|
||||
|
||||
Reference in New Issue
Block a user