t27.aiEnglish

Три бота в глубину

Вы узнаете

Как улей не даёт агентам отвечать друг другу бесконечно.

Telegram не защищает ботов от зацикливания, так что два агента могли бы отвечать друг другу вечно. Это останавливают два числа. MAX_BOT_DEPTH равно 3: человек, затем A, B и C, затем тишина. BOT_REPLY_COOLDOWN_MS равно 5000: бот отвечает другому боту не чаще раза в 5 с. may_answer всегда пропускает человека, а бота — только внутри цепочки и после паузы.

Попробовать

Найдите промежуток, при котором агент B замолкает; затем объясните, почему молчит и C.

Открыть интерактивный урок →

Chain depth: human, A, B, C, then silence
Chain depth: human, A, B, C, then silence ↗

One person asks; agents answer each other. Drag the pause between bot messages: may_answer lets the chain run three bot messages deep and then stops it.

specs/automation/agent-hive-group.t27

// SPDX-License-Identifier: Apache-2.0
// specs/automation/agent-hive-group.t27 -- the agents' hive: one bot per agent, one forum group.
// Host: 999-multibots-telegraf src/core/bot/agentHive/ (addressing, chain, catalogue of bots).
// Trace: owner 2026-10-05, "every agent its own bot and topic, all together in one group".
// Generated: t27c gen-ts; vendored spec and generated constants in src/spec/.
// Claim status: routing contract; a live group needs Bot-to-Bot Communication Mode
// switched on in @BotFather for every bot, which no API call can do.
// Telegram gives no loop protection between bots, so the depth and the cooldown
// below are the only thing that stops two agents from answering each other forever.
// v1 (2026-10-05): who answers, the bot chain limit, the cooldown, the claim window.
// v2 (2026-10-05): the competitor round (999-multibots-telegraf epic #3641) -- typing kept
// alive, one first speaker on the all tag, follow-ups without a tag, /stop and /mute.
// v3 (2026-10-06): the rest of that round -- the seen/done reaction, standing briefs,
// a topic a person deleted, and /summary.
// v4 (2026-10-09): agents hand off in private, bot to bot. Telegram lets a bot send a private
// message to another bot by @username when Bot-to-Bot Communication Mode is on for both
// (owner: "I switched it on exactly for A2A"). A colleague is heard in private only with the
// hand-off tag, an ask is one hop, and the answer is carried to the person, never a new turn,
// and only while its ask is fresh.
// Earlier laws are unchanged; each version only adds, so a host on an older one stays true.
// phi^2 + 1/phi^2 = 3 | TRINITY

module automation::agent_hive_group {
    pub const KIND : str = "automation";
    pub const ID : str = "agent-hive-group";
    pub const REPO : str = "999-multibots-telegraf";
    pub const VERSION : u8 = 4;

    // Bot messages in a row after one human message: human -> A -> B -> C, then silence.
    pub const MAX_BOT_DEPTH : u32 = 3;
    // One agent answers another agent at most this often.
    pub const BOT_REPLY_COOLDOWN_MS : u32 = 5000;
    // How long "create the bot for agent X" waits for Telegram's creation message.
    pub const CLAIM_TTL_MS : u32 = 900000;
    // The tag that calls every agent of the hive at once.
    pub const ALL_TAG : str = "@all";
    // Suggested username of an agent's bot: t27_<agent id>_bot.
    pub const BOT_USERNAME_PREFIX : str = "t27_";
    pub const BOT_USERNAME_SUFFIX : str = "_bot";

    // A person may call an agent by its tag, by the all tag, by replying to it,
    // or by writing in the agent's own topic. Another bot only by tag or reply:
    // a bot never triggers the whole hive, and never wakes a topic's owner by
    // merely writing there.
    fn addressed(tagged: bool, all_tag: bool, reply_to_me: bool, own_topic: bool, from_bot: bool) -> bool {
        return tagged || reply_to_me || (!from_bot && (all_tag || own_topic));
    }

    // depth = how many bot messages already stand between this one and the person.
    fn chain_open(depth: u32) -> bool {
        return depth < MAX_BOT_DEPTH;
    }

    fn cooled(since_last_ms: u32) -> bool {
        return since_last_ms >= BOT_REPLY_COOLDOWN_MS;
    }

    // A person is always answered; a bot only inside the chain and after the cooldown.
    fn may_answer(from_bot: bool, depth: u32, since_last_ms: u32) -> bool {
        return !from_bot || (chain_open(depth) && cooled(since_last_ms));
    }

    fn claim_fresh(age_ms: u32) -> bool {
        return age_ms < CLAIM_TTL_MS;
    }

    // ---- v2 ----

    // Telegram shows "typing" for about five seconds; an agent turn takes up to a minute.
    // The action is sent again this long after the last one, until the answer goes out.
    pub const TYPING_REFRESH_MS : u32 = 4000;
    // On the all tag only this many agents answer at once, best fit first; the rest
    // answer only when tagged. Eight answers to one "@all hi" is a flood, not a team.
    pub const ALL_FIRST_SPEAKERS : u32 = 1;
    // A person's next message in the same topic goes to the agent that answered
    // last, without a new tag, for this long.
    pub const FOLLOW_UP_WINDOW_MS : u32 = 120000;
    // /stop: no bot answers a bot in that chat for this long. People are still answered.
    pub const STOP_WINDOW_MS : u32 = 600000;
    // /mute: the agent answers in that topic only when tagged by name, for this long.
    pub const MUTE_MS : u32 = 3600000;

    fn typing_due(since_last_ms: u32) -> bool {
        return since_last_ms >= TYPING_REFRESH_MS;
    }

    // rank = the agent's place in the best-fit order for this message, from 0.
    fn speaks_on_all(rank: u32) -> bool {
        return rank < ALL_FIRST_SPEAKERS;
    }

    // A bot never follows up: it must tag or reply, as v1 says.
    fn follows_up(from_bot: bool, same_person: bool, same_thread: bool, age_ms: u32) -> bool {
        return !from_bot && same_person && same_thread && age_ms < FOLLOW_UP_WINDOW_MS;
    }

    fn stopped(since_stop_ms: u32) -> bool {
        return since_stop_ms < STOP_WINDOW_MS;
    }

    // A stop cuts bot-to-bot turns only; a person is always heard.
    fn may_answer_after_stop(from_bot: bool, since_stop_ms: u32) -> bool {
        return !from_bot || !stopped(since_stop_ms);
    }

    fn muted(since_mute_ms: u32) -> bool {
        return since_mute_ms < MUTE_MS;
    }

    // A muted agent still answers its own tag: muting a topic must not lose a direct call.
    fn heard_while_muted(tagged: bool, since_mute_ms: u32) -> bool {
        return tagged || !muted(since_mute_ms);
    }

    // ---- v3 ----

    // A reaction is a courtesy: chats may switch reactions off, so the answer counts
    // whether or not the reaction went through. The "seen" mark stays only when the
    // turn ended in an answer; a failed turn takes it away so nobody waits for nothing.
    fn turn_ok(answer_sent: bool, reaction_ok: bool) -> bool {
        return answer_sent;
    }

    fn keeps_seen_mark(answer_sent: bool) -> bool {
        return answer_sent;
    }

    // /brief: standing instructions for a group or one topic, set by an admin only.
    pub const MAX_BRIEF_CHARS : u32 = 1000;
    // Which brief an agent turn carries: 0 none, 1 the group's, 2 the topic's.
    // The narrower one wins; the two are never glued together.
    fn brief_source(has_topic_brief: bool, has_group_brief: bool) -> u8 {
        if has_topic_brief { return 2; }
        if has_group_brief { return 1; }
        return 0;
    }

    // An empty /brief is accepted: it clears the brief, it is not refused.
    fn brief_fits(chars: u32) -> bool {
        return chars <= MAX_BRIEF_CHARS;
    }

    fn may_set_brief(is_admin: bool, from_bot: bool) -> bool {
        return is_admin && !from_bot;
    }

    // A person deleted an agent's topic: the stored id is stale, so it is recreated.
    // Without rights the recreate fails every time, so it is tried at most this often.
    pub const TOPIC_RECREATE_COOLDOWN_MS : u32 = 600000;
    fn recreate_topic(stored: bool, thread_found: bool, since_last_try_ms: u32) -> bool {
        return (!stored || !thread_found) && since_last_try_ms >= TOPIC_RECREATE_COOLDOWN_MS;
    }

    // /summary: the topic keeps a bounded log; a bare /summary reads the default count.
    pub const SUMMARY_LOG_SIZE : u32 = 200;
    pub const SUMMARY_DEFAULT : u32 = 50;
    fn summary_count(asked: u32) -> u32 {
        if asked == 0 { return SUMMARY_DEFAULT; }
        if asked > SUMMARY_LOG_SIZE { return SUMMARY_LOG_SIZE; }
        return asked;
    }

    // ---- v4: hand-off in private, bot to bot ----

    // Every private hand-off between two agents starts with this tag, then its kind and the
    // person it is for. Only a hive agent of the same owner can send it, so the person named in
    // it is trusted: the turn runs in that person's name, never the owner's.
    pub const DM_TAG : str = "#a2a";
    pub const DM_ASK : u8 = 1;
    pub const DM_ANSWER : u8 = 2;
    // An answer to a person hands off to at most this many colleagues.
    pub const MAX_DM_HANDOFFS : u32 = 2;

    // In private a person is always heard; a bot only when it is a colleague (an agent bot of
    // the same owner) and its message carries the hand-off tag with a known kind.
    fn dm_heard(from_bot: bool, colleague: bool, tag_ok: bool) -> bool {
        return !from_bot || (colleague && tag_ok);
    }

    // An ask runs the colleague's turn; an answer is only carried to the person. So a private
    // chain is two messages long and cannot loop, whatever the agents write.
    fn dm_runs_turn(kind: u8) -> bool {
        return kind == DM_ASK;
    }
    fn dm_carries(kind: u8) -> bool {
        return kind == DM_ANSWER;
    }

    // An ask is always a known tag; an answer only while its ask is fresh -- a colleague never
    // writes to a person nobody asked it about. Half an hour covers the longest turn.
    pub const DM_ANSWER_WINDOW_MS : u32 = 1800000;
    fn dm_tag_ok(kind: u8, since_ask_ms: u32) -> bool {
        if kind == DM_ASK { return true; }
        if kind == DM_ANSWER { return since_ask_ms < DM_ANSWER_WINDOW_MS; }
        return false;
    }

    // How many of the hand-offs an answer asked for are sent.
    fn dm_handoffs_sent(asked: u32) -> u32 {
        if asked > MAX_DM_HANDOFFS { return MAX_DM_HANDOFFS; }
        return asked;
    }

    test "a person calls an agent by tag, all tag, reply or its own topic" {
        assert(addressed(true, false, false, false, false));
        assert(addressed(false, true, false, false, false));
        assert(addressed(false, false, true, false, false));
        assert(addressed(false, false, false, true, false));
        assert(!addressed(false, false, false, false, false));
    }

    test "a bot calls an agent only by tag or reply" {
        assert(addressed(true, false, false, false, true));
        assert(addressed(false, false, true, false, true));
        assert(!addressed(false, true, false, false, true));
        assert(!addressed(false, false, false, true, true));
        assert(!addressed(false, true, false, true, true));
    }

    test "the bot chain closes after three bot messages" {
        assert(chain_open(0));
        assert(chain_open(2));
        assert(!chain_open(3));
        assert(!chain_open(4));
    }

    test "a bot waits out the cooldown before answering a bot" {
        assert(!cooled(0));
        assert(!cooled(4999));
        assert(cooled(5000));
    }

    test "a person is always answered and a bot only inside the limits" {
        assert(may_answer(false, 9, 0));
        assert(may_answer(true, 2, 5000));
        assert(!may_answer(true, 3, 5000));
        assert(!may_answer(true, 2, 4999));
    }

    test "a creation claim lives fifteen minutes" {
        assert(claim_fresh(0));
        assert(claim_fresh(899999));
        assert(!claim_fresh(900000));
    }

    test "typing is sent again four seconds after the last one" {
        assert(!typing_due(0));
        assert(!typing_due(3999));
        assert(typing_due(4000));
    }

    test "on the all tag only the best fit answers at once" {
        assert(speaks_on_all(0));
        assert(!speaks_on_all(1));
        assert(!speaks_on_all(7));
    }

    test "a person follows up in the same topic for two minutes without a tag" {
        assert(follows_up(false, true, true, 0));
        assert(follows_up(false, true, true, 119999));
        assert(!follows_up(false, true, true, 120000));
        assert(!follows_up(false, false, true, 0));
        assert(!follows_up(false, true, false, 0));
        assert(!follows_up(true, true, true, 0));
    }

    test "a stop silences bots for ten minutes and never a person" {
        assert(!may_answer_after_stop(true, 0));
        assert(!may_answer_after_stop(true, 599999));
        assert(may_answer_after_stop(true, 600000));
        assert(may_answer_after_stop(false, 0));
    }

    test "a muted agent still answers its own tag for the hour" {
        assert(heard_while_muted(true, 0));
        assert(!heard_while_muted(false, 0));
        assert(!heard_while_muted(false, 3599999));
        assert(heard_while_muted(false, 3600000));
    }

    test "the answer counts even when the chat refuses reactions" {
        assert(turn_ok(true, false));
        assert(turn_ok(true, true));
        assert(!turn_ok(false, true));
        assert(keeps_seen_mark(true));
        assert(!keeps_seen_mark(false));
    }

    test "the topic brief wins over the group brief, and only an admin sets one" {
        assert(brief_source(true, true) == 2);
        assert(brief_source(true, false) == 2);
        assert(brief_source(false, true) == 1);
        assert(brief_source(false, false) == 0);
        assert(brief_fits(0));
        assert(brief_fits(1000));
        assert(!brief_fits(1001));
        assert(may_set_brief(true, false));
        assert(!may_set_brief(false, false));
        assert(!may_set_brief(true, true));
    }

    test "a deleted topic is recreated, at most once in ten minutes" {
        assert(recreate_topic(false, false, 600000));
        assert(recreate_topic(false, true, 600000));
        assert(recreate_topic(true, false, 600000));
        assert(!recreate_topic(true, true, 600000));
        assert(!recreate_topic(true, false, 599999));
    }

    test "a summary reads fifty by default and never past the log" {
        assert(summary_count(0) == 50);
        assert(summary_count(1) == 1);
        assert(summary_count(10) == 10);
        assert(summary_count(200) == 200);
        assert(summary_count(201) == 200);
    }

    test "in private a colleague is heard only with the hand-off tag" {
        assert(dm_heard(false, false, false));
        assert(dm_heard(true, true, true));
        assert(!dm_heard(true, false, true));
        assert(!dm_heard(true, true, false));
    }

    test "an ask runs a turn, an answer is only carried" {
        assert(DM_ASK == 1);
        assert(DM_ANSWER == 2);
        assert(dm_runs_turn(DM_ASK));
        assert(!dm_runs_turn(DM_ANSWER));
        assert(!dm_runs_turn(0));
        assert(dm_carries(DM_ANSWER));
        assert(!dm_carries(DM_ASK));
        assert(!dm_carries(0));
    }

    test "an answer is carried only while its ask is fresh" {
        assert(DM_ANSWER_WINDOW_MS == 1800000);
        assert(dm_tag_ok(DM_ASK, 4000000000));
        assert(dm_tag_ok(DM_ANSWER, 0));
        assert(dm_tag_ok(DM_ANSWER, 1799999));
        assert(!dm_tag_ok(DM_ANSWER, 1800000));
        assert(!dm_tag_ok(DM_ANSWER, 4000000000));
        assert(!dm_tag_ok(0, 0));
        assert(!dm_tag_ok(3, 0));
    }

    test "an answer hands off to at most two colleagues" {
        assert(MAX_DM_HANDOFFS == 2);
        assert(dm_handoffs_sent(0) == 0);
        assert(dm_handoffs_sent(1) == 1);
        assert(dm_handoffs_sent(2) == 2);
        assert(dm_handoffs_sent(3) == 2);
        assert(dm_handoffs_sent(100) == 2);
    }

    test "VERSION says what the header's last note says" {
        assert(VERSION == 4);
    }
}

// phi^2 + 1/phi^2 = 3 | TRINITY

Открыть спеку урока в плеере ↗

Все уроки